diff --git a/client/am/console/src/main/java/org/apache/syncope/client/console/clientapps/ClientAppModalPanelBuilder.java b/client/am/console/src/main/java/org/apache/syncope/client/console/clientapps/ClientAppModalPanelBuilder.java index 7c2e2532b0a..10044edcd27 100644 --- a/client/am/console/src/main/java/org/apache/syncope/client/console/clientapps/ClientAppModalPanelBuilder.java +++ b/client/am/console/src/main/java/org/apache/syncope/client/console/clientapps/ClientAppModalPanelBuilder.java @@ -196,7 +196,7 @@ private class Profile extends AbstractModalPanel { protected Iterator getChoices(final String input) { return realmRestClient.search(fullRealmsTree ? RealmsUtils.buildBaseQuery() - : RealmsUtils.buildKeywordQuery(input)).getResult().stream(). + : RealmsUtils.buildNameQuery(input)).getResult().stream(). map(RealmTO::getFullPath).iterator(); } }; diff --git a/client/idm/console/src/main/java/org/apache/syncope/client/console/panels/ConnObjectListViewPanel.java b/client/idm/console/src/main/java/org/apache/syncope/client/console/panels/ConnObjectListViewPanel.java index b305b968191..b059363ee55 100644 --- a/client/idm/console/src/main/java/org/apache/syncope/client/console/panels/ConnObjectListViewPanel.java +++ b/client/idm/console/src/main/java/org/apache/syncope/client/console/panels/ConnObjectListViewPanel.java @@ -161,8 +161,7 @@ public void onClick(final AjaxRequestTarget target) { List listOfItems = reloadItems(resource.getKey(), anyType, null, null); - ListViewPanel.Builder builder = new ListViewPanel.Builder<>( - ConnObject.class, pageRef) { + ListViewPanel.Builder builder = new ListViewPanel.Builder<>(ConnObject.class, pageRef) { private static final long serialVersionUID = -8251750413385566738L; diff --git a/client/idm/console/src/main/java/org/apache/syncope/client/console/status/ReconTaskPanel.java b/client/idm/console/src/main/java/org/apache/syncope/client/console/status/ReconTaskPanel.java index 98514763174..0f656fd1001 100644 --- a/client/idm/console/src/main/java/org/apache/syncope/client/console/status/ReconTaskPanel.java +++ b/client/idm/console/src/main/java/org/apache/syncope/client/console/status/ReconTaskPanel.java @@ -141,7 +141,7 @@ protected Iterator getChoices(final String input) { return (RealmsUtils.checkInput(input) ? (realmRestClient.search(fullRealmsTree ? RealmsUtils.buildBaseQuery() - : RealmsUtils.buildKeywordQuery(input)).getResult()) + : RealmsUtils.buildNameQuery(input)).getResult()) : List.of()).stream(). map(RealmTO::getFullPath).iterator(); } diff --git a/client/idm/console/src/main/java/org/apache/syncope/client/console/wizards/resources/ConnectorDetailsPanel.java b/client/idm/console/src/main/java/org/apache/syncope/client/console/wizards/resources/ConnectorDetailsPanel.java index 1918fa55d6f..90274ea2339 100644 --- a/client/idm/console/src/main/java/org/apache/syncope/client/console/wizards/resources/ConnectorDetailsPanel.java +++ b/client/idm/console/src/main/java/org/apache/syncope/client/console/wizards/resources/ConnectorDetailsPanel.java @@ -69,7 +69,7 @@ protected Iterator getChoices(final String input) { return (RealmsUtils.checkInput(input) ? (realmRestClient.search(fullRealmsTree ? RealmsUtils.buildBaseQuery() - : RealmsUtils.buildKeywordQuery(input)).getResult()) + : RealmsUtils.buildNameQuery(input)).getResult()) : List.of()).stream(). map(RealmTO::getFullPath).iterator(); } diff --git a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/commons/RealmsUtils.java b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/commons/RealmsUtils.java index c578e594256..6a4375fd947 100644 --- a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/commons/RealmsUtils.java +++ b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/commons/RealmsUtils.java @@ -38,8 +38,8 @@ public static boolean checkInput(final String input) { return StringUtils.isNotBlank(input) && !"*".equals(input); } - public static RealmQuery buildKeywordQuery(final String input) { - return new RealmQuery.Builder().keyword(input.contains("*") ? input : "*" + input + "*").build(); + public static RealmQuery buildNameQuery(final String input) { + return new RealmQuery.Builder().fiql("name=~" + (input.contains("*") ? input : "*" + input + "*")).build(); } public static RealmQuery buildBaseQuery() { diff --git a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/panels/RealmChoicePanel.java b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/panels/RealmChoicePanel.java index 401fe876772..612109715d6 100644 --- a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/panels/RealmChoicePanel.java +++ b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/panels/RealmChoicePanel.java @@ -124,8 +124,8 @@ protected List> load() { Stream> full; if (fullRealmsTree) { full = map.values().stream(). - map(realmTOListPair -> - Pair.of(realmTOListPair.getLeft().getFullPath(), realmTOListPair.getKey())). + map(realmTOListPair + -> Pair.of(realmTOListPair.getLeft().getFullPath(), realmTOListPair.getKey())). sorted(Comparator.comparing(Pair::getLeft)); } else { full = map.entrySet().stream(). @@ -163,7 +163,7 @@ protected List load() { String rootRealmName = StringUtils.substringAfterLast(rootRealm, "/"); List realmTOs = realmRestClient.search( - RealmsUtils.buildKeywordQuery(SyncopeConstants.ROOT_REALM.equals(rootRealm) + RealmsUtils.buildNameQuery(SyncopeConstants.ROOT_REALM.equals(rootRealm) ? SyncopeConstants.ROOT_REALM : rootRealmName)).getResult(); return realmTOs.stream(). @@ -455,7 +455,7 @@ public final RealmChoicePanel reloadRealmTree(final AjaxRequestTarget target, fi protected Map>> reloadRealmParentMap() { List realmsToList = realmRestClient.search(fullRealmsTree ? RealmsUtils.buildBaseQuery() - : RealmsUtils.buildKeywordQuery(searchQuery)).getResult(); + : RealmsUtils.buildNameQuery(searchQuery)).getResult(); return reloadRealmParentMap(realmsToList.stream(). sorted(Comparator.comparing(RealmTO::getName)). diff --git a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/SchedTaskWizardBuilder.java b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/SchedTaskWizardBuilder.java index 6f612e41af1..259f40aba62 100644 --- a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/SchedTaskWizardBuilder.java +++ b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/SchedTaskWizardBuilder.java @@ -124,7 +124,7 @@ protected WizardModel buildModelSteps(final SchedTaskTO modelObject, final Wizar protected List searchRealms(final String realmQuery) { return realmRestClient.search(fullRealmsTree ? RealmsUtils.buildBaseQuery() - : RealmsUtils.buildKeywordQuery(realmQuery)). + : RealmsUtils.buildNameQuery(realmQuery)). getResult().stream().map(RealmTO::getFullPath).collect(Collectors.toList()); } diff --git a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wizards/any/Details.java b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wizards/any/Details.java index 478d1d1fae9..665e3f487e7 100644 --- a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wizards/any/Details.java +++ b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wizards/any/Details.java @@ -96,7 +96,7 @@ protected Iterator getChoices(final String input) { ? getRealmsFromLinks(realms.getRealmChoicePanel().getLinks()) : (fullRealmsTree ? realmRestClient.search(RealmsUtils.buildBaseQuery()) - : realmRestClient.search(RealmsUtils.buildKeywordQuery(input))).getResult()). + : realmRestClient.search(RealmsUtils.buildNameQuery(input))).getResult()). stream().map(RealmTO::getFullPath). filter(fullPath -> authRealms.stream().anyMatch( authRealm -> fullPath.startsWith(authRealm))).iterator(); diff --git a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wizards/role/RoleWizardBuilder.java b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wizards/role/RoleWizardBuilder.java index ab836e14be8..a1d472ab95c 100644 --- a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wizards/role/RoleWizardBuilder.java +++ b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wizards/role/RoleWizardBuilder.java @@ -190,7 +190,7 @@ public Realms(final RoleTO modelObject) { protected Iterator getChoices(final String input) { return realmRestClient.search(fullRealmsTree ? RealmsUtils.buildBaseQuery() - : RealmsUtils.buildKeywordQuery(input)).getResult().stream(). + : RealmsUtils.buildNameQuery(input)).getResult().stream(). map(RealmTO::getFullPath).iterator(); } }; diff --git a/client/idrepo/lib/src/main/java/org/apache/syncope/client/lib/SyncopeClient.java b/client/idrepo/lib/src/main/java/org/apache/syncope/client/lib/SyncopeClient.java index 703097f4246..c886fb5cdc8 100644 --- a/client/idrepo/lib/src/main/java/org/apache/syncope/client/lib/SyncopeClient.java +++ b/client/idrepo/lib/src/main/java/org/apache/syncope/client/lib/SyncopeClient.java @@ -50,6 +50,7 @@ import org.apache.syncope.common.lib.search.ConnObjectTOFiqlSearchConditionBuilder; import org.apache.syncope.common.lib.search.GroupFiqlSearchConditionBuilder; import org.apache.syncope.common.lib.search.OrderByClauseBuilder; +import org.apache.syncope.common.lib.search.RealmFiqlSearchConditionBuilder; import org.apache.syncope.common.lib.search.UserFiqlSearchConditionBuilder; import org.apache.syncope.common.lib.to.UserTO; import org.apache.syncope.common.rest.api.Preference; @@ -77,6 +78,15 @@ public record JwtInfo(String value, OffsetDateTime expiration) } + /** + * Returns a new instance of {@link RealmFiqlSearchConditionBuilder}, for assisted building of FIQL queries. + * + * @return default instance of {@link RealmFiqlSearchConditionBuilder} + */ + public static RealmFiqlSearchConditionBuilder getRealmFiqlSearchConditionBuilder() { + return new RealmFiqlSearchConditionBuilder(); + } + /** * Returns a new instance of {@link UserFiqlSearchConditionBuilder}, for assisted building of FIQL queries. * diff --git a/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/search/RealmCompleteCondition.java b/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/search/RealmCompleteCondition.java new file mode 100644 index 00000000000..49976b2e773 --- /dev/null +++ b/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/search/RealmCompleteCondition.java @@ -0,0 +1,22 @@ +/* + * 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.common.lib.search; + +public interface RealmCompleteCondition extends SyncopeCompleteCondition { +} diff --git a/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/search/RealmFiqlSearchConditionBuilder.java b/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/search/RealmFiqlSearchConditionBuilder.java new file mode 100644 index 00000000000..7ee9d319389 --- /dev/null +++ b/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/search/RealmFiqlSearchConditionBuilder.java @@ -0,0 +1,50 @@ +/* + * 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.common.lib.search; + +import java.util.Map; + +public class RealmFiqlSearchConditionBuilder + extends AbstractFiqlSearchConditionBuilder { + + private static final long serialVersionUID = 324753886224642253L; + + protected static class Builder extends AbstractFiqlSearchConditionBuilder.Builder< + RealmProperty, RealmPartialCondition, RealmCompleteCondition> + implements RealmProperty, RealmPartialCondition, RealmCompleteCondition { + + public Builder(final Map properties) { + super(properties); + } + + public Builder(final RealmFiqlSearchConditionBuilder.Builder parent) { + super(parent); + } + } + + @Override + protected Builder newBuilderInstance() { + return new Builder(properties); + } + + @Override + public RealmProperty is(final String property) { + return newBuilderInstance().is(property); + } +} diff --git a/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/search/RealmPartialCondition.java b/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/search/RealmPartialCondition.java new file mode 100644 index 00000000000..dda92a61d98 --- /dev/null +++ b/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/search/RealmPartialCondition.java @@ -0,0 +1,22 @@ +/* + * 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.common.lib.search; + +public interface RealmPartialCondition extends SyncopePartialCondition { +} diff --git a/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/search/RealmProperty.java b/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/search/RealmProperty.java new file mode 100644 index 00000000000..1cffb03f281 --- /dev/null +++ b/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/search/RealmProperty.java @@ -0,0 +1,22 @@ +/* + * 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.common.lib.search; + +public interface RealmProperty extends SyncopeProperty { +} diff --git a/common/idrepo/rest-api/src/main/java/org/apache/syncope/common/rest/api/beans/AnyQuery.java b/common/idrepo/rest-api/src/main/java/org/apache/syncope/common/rest/api/beans/AnyQuery.java index 5d13ea2b8b5..893daba106d 100644 --- a/common/idrepo/rest-api/src/main/java/org/apache/syncope/common/rest/api/beans/AnyQuery.java +++ b/common/idrepo/rest-api/src/main/java/org/apache/syncope/common/rest/api/beans/AnyQuery.java @@ -74,7 +74,7 @@ public Builder fiql(final String fiql) { + "primarily meant for containing Users, Groups and Any Objects", schema = @Schema(implementation = String.class, defaultValue = SyncopeConstants.ROOT_REALM, externalDocs = @ExternalDocumentation(description = "Apache Syncope Reference Guide", - url = "https://syncope.apache.org/docs/3.0/reference-guide.html#realms"))) + url = "https://syncope.apache.org/docs/4.0/reference-guide.html#realms"))) public String getRealm() { return realm; } @@ -120,7 +120,7 @@ public String getFiql() { + "feed.", example = "username==rossini", schema = @Schema(implementation = String.class, externalDocs = @ExternalDocumentation(description = "Apache Syncope Reference Guide", - url = "https://syncope.apache.org/docs/3.0/reference-guide.html#search"))) + url = "https://syncope.apache.org/docs/4.0/reference-guide.html#search"))) @QueryParam(JAXRSService.PARAM_FIQL) public void setFiql(final String fiql) { this.fiql = fiql; diff --git a/common/idrepo/rest-api/src/main/java/org/apache/syncope/common/rest/api/beans/RealmQuery.java b/common/idrepo/rest-api/src/main/java/org/apache/syncope/common/rest/api/beans/RealmQuery.java index badd1546105..2d23920a849 100644 --- a/common/idrepo/rest-api/src/main/java/org/apache/syncope/common/rest/api/beans/RealmQuery.java +++ b/common/idrepo/rest-api/src/main/java/org/apache/syncope/common/rest/api/beans/RealmQuery.java @@ -18,6 +18,9 @@ */ package org.apache.syncope.common.rest.api.beans; +import io.swagger.v3.oas.annotations.ExternalDocumentation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.ws.rs.QueryParam; import java.util.Collection; import java.util.HashSet; @@ -27,6 +30,7 @@ import java.util.stream.Stream; import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.apache.syncope.common.rest.api.service.JAXRSService; public class RealmQuery extends AbstractQuery { @@ -39,8 +43,8 @@ protected RealmQuery newInstance() { return new RealmQuery(); } - public Builder keyword(final String keyword) { - getInstance().setKeyword(keyword); + public Builder fiql(final String fiql) { + getInstance().setFiql(fiql); return this; } @@ -63,17 +67,23 @@ public Builder bases(final Collection bases) { } } - private String keyword; + private String fiql; private Set bases; - public String getKeyword() { - return keyword; + public String getFiql() { + return fiql; } - @QueryParam("keyword") - public void setKeyword(final String keyword) { - this.keyword = keyword; + @Parameter(name = JAXRSService.PARAM_FIQL, description = "Feed Item Query Language (FIQL, pronounced “fickle”) is " + + "a simple but flexible, URI-friendly syntax for expressing filters across the entries in a syndicated " + + "feed.", example = "name==department1", schema = + @Schema(implementation = String.class, externalDocs = + @ExternalDocumentation(description = "Apache Syncope Reference Guide", + url = "https://syncope.apache.org/docs/4.0/reference-guide.html#search"))) + @QueryParam(JAXRSService.PARAM_FIQL) + public void setFiql(final String fiql) { + this.fiql = fiql; } public Set getBases() { @@ -99,7 +109,7 @@ public boolean equals(final Object obj) { RealmQuery other = (RealmQuery) obj; return new EqualsBuilder(). appendSuper(super.equals(obj)). - append(keyword, other.keyword). + append(fiql, other.fiql). append(bases, other.bases). build(); } @@ -108,7 +118,7 @@ public boolean equals(final Object obj) { public int hashCode() { return new HashCodeBuilder(). appendSuper(super.hashCode()). - append(keyword). + append(fiql). append(bases). build(); } diff --git a/core/idm/logic/src/main/java/org/apache/syncope/core/logic/ReconciliationLogic.java b/core/idm/logic/src/main/java/org/apache/syncope/core/logic/ReconciliationLogic.java index 68de5c981e8..978b3ec316a 100644 --- a/core/idm/logic/src/main/java/org/apache/syncope/core/logic/ReconciliationLogic.java +++ b/core/idm/logic/src/main/java/org/apache/syncope/core/logic/ReconciliationLogic.java @@ -598,7 +598,7 @@ public List push( orElseThrow(() -> new NotFoundException("Realm " + realm)); Set adminRealms = RealmUtils.getEffective(AuthContextUtils.getAuthorizations().get(entitlement), realm); - SearchCond effectiveCond = searchCond == null ? anyUtils.dao().getAllMatchingCond() : searchCond; + SearchCond effectiveCond = searchCond == null ? anySearchDAO.getAllMatchingCond() : searchCond; List matching; if (spec.getIgnorePaging()) { diff --git a/core/idm/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/IdMRESTCXFContext.java b/core/idm/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/IdMRESTCXFContext.java index bf74c2ee2a1..70d773f86b4 100644 --- a/core/idm/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/IdMRESTCXFContext.java +++ b/core/idm/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/IdMRESTCXFContext.java @@ -27,7 +27,7 @@ import org.apache.syncope.core.logic.RemediationLogic; import org.apache.syncope.core.logic.ResourceLogic; import org.apache.syncope.core.persistence.api.entity.AnyUtilsFactory; -import org.apache.syncope.core.persistence.api.search.SearchCondVisitor; +import org.apache.syncope.core.persistence.api.search.AnySearchCondVisitor; import org.apache.syncope.core.rest.cxf.service.ConnectorServiceImpl; import org.apache.syncope.core.rest.cxf.service.ReconciliationServiceImpl; import org.apache.syncope.core.rest.cxf.service.RemediationServiceImpl; @@ -48,7 +48,7 @@ public ConnectorService connectorService(final ConnectorLogic connectorLogic) { @ConditionalOnMissingBean @Bean public ReconciliationService reconciliationService( - final SearchCondVisitor searchCondVisitor, + final AnySearchCondVisitor searchCondVisitor, final ReconciliationLogic reconciliationLogic) { return new ReconciliationServiceImpl(searchCondVisitor, reconciliationLogic); diff --git a/core/idm/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/ReconciliationServiceImpl.java b/core/idm/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/ReconciliationServiceImpl.java index 43480eab2b4..7cc4829b40c 100644 --- a/core/idm/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/ReconciliationServiceImpl.java +++ b/core/idm/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/ReconciliationServiceImpl.java @@ -48,16 +48,18 @@ import org.apache.syncope.common.rest.api.service.ReconciliationService; import org.apache.syncope.core.logic.ReconciliationLogic; import org.apache.syncope.core.persistence.api.dao.search.SearchCond; +import org.apache.syncope.core.persistence.api.search.AnySearchCondVisitor; import org.apache.syncope.core.persistence.api.search.FilterVisitor; -import org.apache.syncope.core.persistence.api.search.SearchCondVisitor; import org.apache.syncope.core.spring.security.AuthContextUtils; import org.identityconnectors.framework.common.objects.filter.Filter; -public class ReconciliationServiceImpl extends AbstractSearchService implements ReconciliationService { +public class ReconciliationServiceImpl + extends AbstractSearchService + implements ReconciliationService { protected final ReconciliationLogic logic; - public ReconciliationServiceImpl(final SearchCondVisitor searchCondVisitor, final ReconciliationLogic logic) { + public ReconciliationServiceImpl(final AnySearchCondVisitor searchCondVisitor, final ReconciliationLogic logic) { super(searchCondVisitor); this.logic = logic; } @@ -150,7 +152,7 @@ public Response push(final AnyQuery query, final CSVPushSpec spec) { SearchCond searchCond = StringUtils.isBlank(query.getFiql()) ? null - : getSearchCond(query.getFiql(), realm); + : getSearchCond(query.getFiql()); StreamingOutput sout = os -> logic.push( searchCond, diff --git a/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/GroupLogic.java b/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/GroupLogic.java index 1acd1039ff8..7b1929363c3 100644 --- a/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/GroupLogic.java +++ b/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/GroupLogic.java @@ -172,7 +172,7 @@ public Page search( Set authRealms = RealmUtils.getEffective( AuthContextUtils.getAuthorizations().get(IdRepoEntitlement.GROUP_SEARCH), realm); - SearchCond effectiveCond = searchCond == null ? groupDAO.getAllMatchingCond() : searchCond; + SearchCond effectiveCond = searchCond == null ? searchDAO.getAllMatchingCond() : searchCond; long count = searchDAO.count(base, recursive, authRealms, effectiveCond, AnyTypeKind.GROUP); diff --git a/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/RealmLogic.java b/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/RealmLogic.java index ae2cdeaf9cb..bf297dac359 100644 --- a/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/RealmLogic.java +++ b/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/RealmLogic.java @@ -121,8 +121,8 @@ protected void securityChecks(final Set effectiveRealms, final String re @PreAuthorize("isAuthenticated()") @Transactional(readOnly = true) public Page search( - final String keyword, final Set bases, + final SearchCond searchCond, final Pageable pageable) { Set baseRealms = new HashSet<>(); @@ -135,14 +135,15 @@ public Page search( } } - long count = realmSearchDAO.countDescendants(baseRealms, keyword); + SearchCond effectiveCond = searchCond == null ? realmSearchDAO.getAllMatchingCond() : searchCond; + + long count = realmSearchDAO.count(baseRealms, effectiveCond); Set authorizations = AuthContextUtils.getAuthorizations(). getOrDefault(IdRepoEntitlement.REALM_SEARCH, Set.of()); - List result = realmSearchDAO.findDescendants(baseRealms, keyword, pageable).stream(). + List result = realmSearchDAO.search(baseRealms, effectiveCond, pageable).stream(). map(realm -> binder.getRealmTO( - realm, authorizations.stream(). - anyMatch(auth -> realm.getFullPath().startsWith(auth)))). + realm, authorizations.stream().anyMatch(auth -> realm.getFullPath().startsWith(auth)))). sorted(Comparator.comparing(RealmTO::getFullPath)). toList(); diff --git a/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/UserLogic.java b/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/UserLogic.java index 87a9fecdac1..51039050ee6 100644 --- a/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/UserLogic.java +++ b/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/UserLogic.java @@ -185,7 +185,7 @@ public Page search( Set authRealms = RealmUtils.getEffective( AuthContextUtils.getAuthorizations().get(IdRepoEntitlement.USER_SEARCH), realm); - SearchCond effectiveCond = searchCond == null ? userDAO.getAllMatchingCond() : searchCond; + SearchCond effectiveCond = searchCond == null ? searchDAO.getAllMatchingCond() : searchCond; long count = searchDAO.count(base, recursive, authRealms, effectiveCond, AnyTypeKind.USER); diff --git a/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/IdRepoRESTCXFContext.java b/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/IdRepoRESTCXFContext.java index ed422f422f2..a84c9107f1a 100644 --- a/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/IdRepoRESTCXFContext.java +++ b/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/IdRepoRESTCXFContext.java @@ -102,7 +102,8 @@ import org.apache.syncope.core.persistence.api.dao.GroupDAO; import org.apache.syncope.core.persistence.api.dao.UserDAO; import org.apache.syncope.core.persistence.api.entity.EntityFactory; -import org.apache.syncope.core.persistence.api.search.SearchCondVisitor; +import org.apache.syncope.core.persistence.api.search.AnySearchCondVisitor; +import org.apache.syncope.core.persistence.api.search.RealmSearchCondVisitor; import org.apache.syncope.core.rest.cxf.service.AccessTokenServiceImpl; import org.apache.syncope.core.rest.cxf.service.AnyObjectServiceImpl; import org.apache.syncope.core.rest.cxf.service.AnyTypeClassServiceImpl; @@ -346,7 +347,7 @@ public AccessTokenService accessTokenService(final AccessTokenLogic accessTokenL @ConditionalOnMissingBean @Bean public AnyObjectService anyObjectService(final AnyObjectDAO anyObjectDAO, final AnyObjectLogic anyObjectLogic, - final SearchCondVisitor searchCondVisitor) { + final AnySearchCondVisitor searchCondVisitor) { return new AnyObjectServiceImpl(searchCondVisitor, anyObjectDAO, anyObjectLogic); } @@ -395,7 +396,7 @@ public DynRealmService dynRealmService(final DynRealmLogic dynRealmLogic) { @ConditionalOnMissingBean @Bean public GroupService groupService(final GroupDAO groupDAO, final GroupLogic groupLogic, - final SearchCondVisitor searchCondVisitor) { + final AnySearchCondVisitor searchCondVisitor) { return new GroupServiceImpl(searchCondVisitor, groupDAO, groupLogic); } @@ -425,8 +426,8 @@ public PolicyService policyService(final PolicyLogic policyLogic) { @ConditionalOnMissingBean @Bean - public RealmService realmService(final RealmLogic realmLogic) { - return new RealmServiceImpl(realmLogic); + public RealmService realmService(final RealmLogic realmLogic, final RealmSearchCondVisitor searchCondVisitor) { + return new RealmServiceImpl(realmLogic, searchCondVisitor); } @ConditionalOnMissingBean @@ -490,7 +491,7 @@ public UserService userService( final UserDAO userDAO, final UserLogic userLogic, final SyncopeLogic syncopeLogic, - final SearchCondVisitor searchCondVisitor) { + final AnySearchCondVisitor searchCondVisitor) { return new UserServiceImpl(searchCondVisitor, userDAO, userLogic, syncopeLogic); } diff --git a/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/SyncopeOpenApiCustomizer.java b/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/SyncopeOpenApiCustomizer.java index edd778bae8f..97ec348f539 100644 --- a/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/SyncopeOpenApiCustomizer.java +++ b/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/SyncopeOpenApiCustomizer.java @@ -92,7 +92,7 @@ protected void addParameters(final List parameters) { ExternalDocumentation extDoc = new ExternalDocumentation(); extDoc.setDescription("Apache Syncope Reference Guide"); - extDoc.setUrl("https://syncope.apache.org/docs/3.0/reference-guide.html#domains"); + extDoc.setUrl("https://syncope.apache.org/docs/4.0/reference-guide.html#domains"); Schema schema = new Schema<>(); schema.setDescription("Domains are built to facilitate multitenancy."); @@ -113,7 +113,7 @@ protected void addParameters(final List parameters) { ExternalDocumentation extDoc = new ExternalDocumentation(); extDoc.setDescription("Apache Syncope Reference Guide"); - extDoc.setUrl("https://syncope.apache.org/docs/3.0/reference-guide.html#delegation"); + extDoc.setUrl("https://syncope.apache.org/docs/4.0/reference-guide.html#delegation"); Schema schema = new Schema<>(); schema.setDescription("Acton behalf of someone else"); diff --git a/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/AbstractAnyService.java b/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/AbstractAnyService.java index 5137b272e81..3e98e2e49f4 100644 --- a/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/AbstractAnyService.java +++ b/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/AbstractAnyService.java @@ -54,15 +54,16 @@ import org.apache.syncope.core.persistence.api.dao.AnyDAO; import org.apache.syncope.core.persistence.api.dao.NotFoundException; import org.apache.syncope.core.persistence.api.dao.search.SearchCond; -import org.apache.syncope.core.persistence.api.search.SearchCondVisitor; +import org.apache.syncope.core.persistence.api.search.AnySearchCondVisitor; import org.apache.syncope.core.provisioning.api.serialization.POJOHelper; import org.apache.syncope.core.spring.security.SecureRandomUtils; import org.springframework.data.domain.Page; public abstract class AbstractAnyService - extends AbstractSearchService implements AnyService { + extends AbstractSearchService + implements AnyService { - public AbstractAnyService(final SearchCondVisitor searchCondVisitor) { + public AbstractAnyService(final AnySearchCondVisitor searchCondVisitor) { super(searchCondVisitor); } @@ -117,7 +118,7 @@ public PagedResult search(final AnyQuery anyQuery) { String realm = Strings.CS.prependIfMissing(anyQuery.getRealm(), SyncopeConstants.ROOT_REALM); SearchCond searchCond = StringUtils.isBlank(anyQuery.getFiql()) ? null - : getSearchCond(anyQuery.getFiql(), realm); + : getSearchCond(anyQuery.getFiql()); try { Page result = getAnyLogic().search( searchCond, diff --git a/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/AbstractSearchService.java b/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/AbstractSearchService.java index 97afca154d5..4e6ce538357 100644 --- a/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/AbstractSearchService.java +++ b/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/AbstractSearchService.java @@ -24,19 +24,18 @@ import org.apache.syncope.common.lib.SyncopeClientException; import org.apache.syncope.common.lib.types.ClientExceptionType; import org.apache.syncope.core.persistence.api.dao.search.SearchCond; -import org.apache.syncope.core.persistence.api.search.SearchCondVisitor; +import org.apache.syncope.core.persistence.api.search.SyncopeAbstractSearchCondVisitor; -public abstract class AbstractSearchService extends AbstractService { +public abstract class AbstractSearchService extends AbstractService { - protected final SearchCondVisitor searchCondVisitor; + protected final V searchCondVisitor; - public AbstractSearchService(final SearchCondVisitor searchCondVisitor) { + public AbstractSearchService(final V searchCondVisitor) { this.searchCondVisitor = searchCondVisitor; } - protected SearchCond getSearchCond(final String fiql, final String realm) { + protected SearchCond getSearchCond(final String fiql) { try { - searchCondVisitor.setRealm(realm); SearchCondition sc = searchContext.getCondition(fiql, SearchBean.class); sc.accept(searchCondVisitor); diff --git a/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/AnyObjectServiceImpl.java b/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/AnyObjectServiceImpl.java index 8a6adf82780..cb3e9234703 100644 --- a/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/AnyObjectServiceImpl.java +++ b/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/AnyObjectServiceImpl.java @@ -34,7 +34,7 @@ import org.apache.syncope.core.logic.AnyObjectLogic; import org.apache.syncope.core.persistence.api.dao.AnyDAO; import org.apache.syncope.core.persistence.api.dao.AnyObjectDAO; -import org.apache.syncope.core.persistence.api.search.SearchCondVisitor; +import org.apache.syncope.core.persistence.api.search.AnySearchCondVisitor; public class AnyObjectServiceImpl extends AbstractAnyService implements AnyObjectService { @@ -44,7 +44,7 @@ public class AnyObjectServiceImpl extends AbstractAnyService implements GroupService { @@ -40,7 +40,7 @@ public class GroupServiceImpl extends AbstractAnyService implements RealmService { protected final RealmLogic logic; - public RealmServiceImpl(final RealmLogic logic) { + public RealmServiceImpl(final RealmLogic logic, final RealmSearchCondVisitor searchCondVisitor) { + super(searchCondVisitor); this.logic = logic; } @Override public PagedResult search(final RealmQuery query) { - Page result = logic.search( - Optional.ofNullable(query.getKeyword()).map(k -> k.replace('*', '%')).orElse(null), - query.getBases(), - pageable(query)); - return buildPagedResult(result); + SearchCond searchCond = StringUtils.isBlank(query.getFiql()) ? null : getSearchCond(query.getFiql()); + try { + Page result = logic.search( + query.getBases(), + searchCond, + pageable(query)); + return buildPagedResult(result); + } catch (IllegalArgumentException e) { + SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.InvalidSearchParameters); + sce.getElements().add(query.getFiql()); + sce.getElements().add(ExceptionUtils.getRootCauseMessage(e)); + throw sce; + } } @Override diff --git a/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/UserServiceImpl.java b/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/UserServiceImpl.java index 1d34a8ab15a..12a89c52b68 100644 --- a/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/UserServiceImpl.java +++ b/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/UserServiceImpl.java @@ -33,7 +33,7 @@ import org.apache.syncope.core.logic.UserLogic; import org.apache.syncope.core.persistence.api.dao.AnyDAO; import org.apache.syncope.core.persistence.api.dao.UserDAO; -import org.apache.syncope.core.persistence.api.search.SearchCondVisitor; +import org.apache.syncope.core.persistence.api.search.AnySearchCondVisitor; public class UserServiceImpl extends AbstractAnyService implements UserService { @@ -44,7 +44,7 @@ public class UserServiceImpl extends AbstractAnyService protected final SyncopeLogic syncopeLogic; public UserServiceImpl( - final SearchCondVisitor searchCondVisitor, + final AnySearchCondVisitor searchCondVisitor, final UserDAO userDAO, final UserLogic logic, final SyncopeLogic syncopeLogic) { diff --git a/core/idrepo/rest-cxf/src/test/java/org/apache/syncope/core/rest/cxf/service/AnyObjectServiceTest.java b/core/idrepo/rest-cxf/src/test/java/org/apache/syncope/core/rest/cxf/service/AnyObjectServiceTest.java index 055d0a25a35..99cef6896af 100644 --- a/core/idrepo/rest-cxf/src/test/java/org/apache/syncope/core/rest/cxf/service/AnyObjectServiceTest.java +++ b/core/idrepo/rest-cxf/src/test/java/org/apache/syncope/core/rest/cxf/service/AnyObjectServiceTest.java @@ -72,7 +72,7 @@ import org.apache.syncope.core.logic.AnyObjectLogic; import org.apache.syncope.core.persistence.api.dao.AnyObjectDAO; import org.apache.syncope.core.persistence.api.dao.search.SearchCond; -import org.apache.syncope.core.persistence.api.search.SearchCondVisitor; +import org.apache.syncope.core.persistence.api.search.AnySearchCondVisitor; import org.apache.syncope.core.persistence.api.search.SyncopePage; import org.apache.syncope.core.rest.cxf.AddETagFilter; import org.apache.syncope.core.rest.cxf.RestServiceExceptionMapper; @@ -155,7 +155,7 @@ public void setup() { return result; }); - SearchCondVisitor searchCondVisitor = mock(SearchCondVisitor.class); + AnySearchCondVisitor searchCondVisitor = mock(AnySearchCondVisitor.class); when(searchCondVisitor.getQuery()).thenReturn(new SearchCond()); @SuppressWarnings("unchecked") diff --git a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/AnyDAO.java b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/AnyDAO.java index 389943c992c..fd981881126 100644 --- a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/AnyDAO.java +++ b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/AnyDAO.java @@ -22,9 +22,6 @@ import java.util.Collection; import java.util.List; import java.util.Optional; -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.ExternalResource; import org.apache.syncope.core.persistence.api.entity.Schema; @@ -41,32 +38,10 @@ public interface AnyDAO extends DAO { A authFind(String key); - /** - * Find any objects 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 key) 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 keys used as identifiers. - * - * @param expression JEXL expression - * @param value derived attribute value - * @param ignoreCaseMatch whether comparison for string values should take case into account or not - * @return list of any objects - */ - List findByDerAttrValue(String expression, String value, boolean ignoreCaseMatch); - List findByResourcesContaining(ExternalResource resource); Page findAll(Pageable pageable); - /** - * @return the search condition to match all entities - */ - default SearchCond getAllMatchingCond() { - AnyCond idCond = new AnyCond(AttrCond.Type.ISNOTNULL); - idCond.setSchema("id"); - return SearchCond.of(idCond); - } - AllowedSchemas findAllowedSchemas(A any, Class reference); List findDynRealms(String key); diff --git a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/AnySearchDAO.java b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/AnySearchDAO.java index 85dc4faf343..2e0e0b7d910 100644 --- a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/AnySearchDAO.java +++ b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/AnySearchDAO.java @@ -21,6 +21,8 @@ import java.util.List; import java.util.Set; import org.apache.syncope.common.lib.types.AnyTypeKind; +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.Realm; @@ -29,12 +31,31 @@ public interface AnySearchDAO { + /** + * Find any objects 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 key) 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 keys used as identifiers. + * + * @param expression JEXL expression + * @param value derived attribute value + * @param ignoreCaseMatch whether comparison for string values should take case into account or not + * @param kind any type kind + * @param any + * @return list of any objects + */ + List findByDerAttrValue( + String expression, + String value, + boolean ignoreCaseMatch, + AnyTypeKind kind); + /** * @param base Realm to start searching from * @param recursive whether search should recursively include results from child Realms * @param adminRealms realms for which the caller owns the proper entitlement(s) * @param searchCondition the search condition - * @param kind any object + * @param kind any type kind * @return size of search result */ long count( @@ -55,7 +76,7 @@ long count( /** * @param searchCondition the search condition * @param orderBy list of ordering clauses - * @param kind any object + * @param kind any type kind * @param any * @return the list of any objects matching the given search condition */ @@ -67,7 +88,7 @@ long count( * @param adminRealms realms for which the caller owns the proper entitlement(s) * @param searchCondition the search condition * @param pageable paging information - * @param kind any object + * @param kind any type kind * @param any * @return the list of any objects matching the given search condition (in the given page) */ @@ -78,4 +99,13 @@ List search( SearchCond searchCondition, Pageable pageable, AnyTypeKind kind); + + /** + * @return the search condition to match all entities + */ + default SearchCond getAllMatchingCond() { + AnyCond idCond = new AnyCond(AttrCond.Type.ISNOTNULL); + idCond.setSchema("id"); + return SearchCond.of(idCond); + } } diff --git a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/RealmSearchDAO.java b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/RealmSearchDAO.java index 3b3a740b5f5..e72e8656534 100644 --- a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/RealmSearchDAO.java +++ b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/RealmSearchDAO.java @@ -22,6 +22,9 @@ import java.util.List; import java.util.Optional; import java.util.Set; +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.Realm; import org.springframework.data.domain.Pageable; @@ -33,15 +36,7 @@ public interface RealmSearchDAO { List findChildren(Realm realm); - long countDescendants(String base, String keyword); - - long countDescendants(Set bases, String keyword); - - List findDescendants(String base, String keyword, Pageable pageable); - - List findDescendants(Set bases, String keyword, Pageable pageable); - - List findDescendants(String base, String prefix); + List findDescendants(String base, String prefix); default void findAncestors(final List result, final Realm realm) { if (realm.getParent() != null && !result.contains(realm.getParent())) { @@ -56,4 +51,27 @@ default List findAncestors(Realm realm) { findAncestors(result, realm); return result; } + + /** + * Find realmss 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 key) 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 keys used as identifiers. + * + * @param expression JEXL expression + * @param value derived attribute value + * @param ignoreCaseMatch whether comparison for string values should take case into account or not + * @return list of realms + */ + List findByDerAttrValue(String expression, String value, boolean ignoreCaseMatch); + + long count(Set bases, SearchCond cond); + + List search(Set bases, SearchCond cond, Pageable pageable); + + default SearchCond getAllMatchingCond() { + AnyCond idCond = new AnyCond(AttrCond.Type.ISNOTNULL); + idCond.setSchema("id"); + return SearchCond.of(idCond); + } } diff --git a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/search/AnySearchCondVisitor.java b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/search/AnySearchCondVisitor.java new file mode 100644 index 00000000000..d31f7f5995d --- /dev/null +++ b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/search/AnySearchCondVisitor.java @@ -0,0 +1,31 @@ +/* + * 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.api.search; + +import java.util.Optional; +import org.apache.syncope.common.lib.search.SpecialAttr; + +public class AnySearchCondVisitor extends SyncopeAbstractSearchCondVisitor { + + + @Override + protected Optional getSpecialAttrName(final String propertyName) { + return SpecialAttr.fromString(propertyName); + } +} diff --git a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/search/RealmSearchCondVisitor.java b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/search/RealmSearchCondVisitor.java new file mode 100644 index 00000000000..b31cd96c20c --- /dev/null +++ b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/search/RealmSearchCondVisitor.java @@ -0,0 +1,31 @@ +/* + * 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.api.search; + +import java.util.Optional; +import org.apache.syncope.common.lib.search.SpecialAttr; + +public class RealmSearchCondVisitor extends SyncopeAbstractSearchCondVisitor { + + + @Override + protected Optional getSpecialAttrName(final String propertyName) { + return Optional.empty(); + } +} diff --git a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/search/SearchCondConverter.java b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/search/SearchCondConverter.java index 83c1486f122..3a8ada55725 100644 --- a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/search/SearchCondConverter.java +++ b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/search/SearchCondConverter.java @@ -37,19 +37,16 @@ public final class SearchCondConverter { /** * Parses a FIQL expression into Syncope's {@link SearchCond}, using {@link SyncopeFiqlParser}. * + * @param concrete {@link SyncopeAbstractSearchCondVisitor} implementation * @param visitor visitor instance * @param fiql FIQL string - * @param realms optional realm to provide to {@link SearchCondVisitor} * @return {@link SearchCond} instance for given FIQL expression */ - public static SearchCond convert(final SearchCondVisitor visitor, final String fiql, final String... realms) { + public static SearchCond convert(final V visitor, final String fiql) { SyncopeFiqlParser parser = new SyncopeFiqlParser<>( SearchBean.class, AbstractFiqlSearchConditionBuilder.CONTEXTUAL_PROPERTIES); try { - if (realms != null && realms.length > 0) { - visitor.setRealm(realms[0]); - } SearchCondition sc = parser.parse(URLDecoder.decode(fiql, StandardCharsets.UTF_8)); sc.accept(visitor); diff --git a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/search/SearchCondVisitor.java b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/search/SyncopeAbstractSearchCondVisitor.java similarity index 96% rename from core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/search/SearchCondVisitor.java rename to core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/search/SyncopeAbstractSearchCondVisitor.java index e5f3f32797d..999e21c994a 100644 --- a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/search/SearchCondVisitor.java +++ b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/search/SyncopeAbstractSearchCondVisitor.java @@ -48,20 +48,11 @@ /** * Visits CXF's {@link SearchBean} and produces {@link SearchCond}. */ -public class SearchCondVisitor extends AbstractSearchConditionVisitor { - - protected static final ThreadLocal REALM = new ThreadLocal<>(); +public abstract class SyncopeAbstractSearchCondVisitor + extends AbstractSearchConditionVisitor { protected static final ThreadLocal SEARCH_COND = new ThreadLocal<>(); - public SearchCondVisitor() { - super(null); - } - - public void setRealm(final String realm) { - REALM.set(realm); - } - protected static AttrCond createAttrCond(final String schema) { AttrCond attrCond = SearchableFields.contains(schema) ? new AnyCond() @@ -84,6 +75,7 @@ protected static ConditionType getConditionType(final SearchCondition sfsc && sc.getConditionType() == ConditionType.CUSTOM) { + switch (sfsc.getOperator()) { case SyncopeFiqlParser.IEQ: ct = ConditionType.EQUALS; @@ -102,10 +94,16 @@ protected static ConditionType getConditionType(final SearchCondition getSpecialAttrName(String propertyName); + @SuppressWarnings("ConvertToStringSwitch") protected SearchCond visitPrimitive(final SearchCondition sc) { String name = getRealPropertyName(sc.getStatement().getProperty()); - Optional specialAttrName = SpecialAttr.fromString(name); + Optional specialAttrName = getSpecialAttrName(name); String value = getValue(sc); Optional specialAttrValue = SpecialAttr.fromString(value); diff --git a/core/persistence-api/src/test/java/org/apache/syncope/core/persistence/api/search/SearchCondConverterTest.java b/core/persistence-api/src/test/java/org/apache/syncope/core/persistence/api/search/SearchCondConverterTest.java index 6cb723c0cbb..d227978d8da 100644 --- a/core/persistence-api/src/test/java/org/apache/syncope/core/persistence/api/search/SearchCondConverterTest.java +++ b/core/persistence-api/src/test/java/org/apache/syncope/core/persistence/api/search/SearchCondConverterTest.java @@ -41,7 +41,7 @@ public class SearchCondConverterTest { - private static final SearchCondVisitor VISITOR = new SearchCondVisitor(); + private static final AnySearchCondVisitor VISITOR = new AnySearchCondVisitor(); @Test public void eq() { diff --git a/core/persistence-common/src/main/java/org/apache/syncope/core/persistence/common/CommonPersistenceContext.java b/core/persistence-common/src/main/java/org/apache/syncope/core/persistence/common/CommonPersistenceContext.java index 5c697266861..01f40cb70a1 100644 --- a/core/persistence-common/src/main/java/org/apache/syncope/core/persistence/common/CommonPersistenceContext.java +++ b/core/persistence-common/src/main/java/org/apache/syncope/core/persistence/common/CommonPersistenceContext.java @@ -31,7 +31,8 @@ import org.apache.syncope.core.persistence.api.entity.EntityFactory; import org.apache.syncope.core.persistence.api.entity.am.ClientAppUtilsFactory; import org.apache.syncope.core.persistence.api.entity.policy.PolicyUtilsFactory; -import org.apache.syncope.core.persistence.api.search.SearchCondVisitor; +import org.apache.syncope.core.persistence.api.search.AnySearchCondVisitor; +import org.apache.syncope.core.persistence.api.search.RealmSearchCondVisitor; import org.apache.syncope.core.persistence.api.utils.RealmUtils; import org.apache.syncope.core.persistence.common.attrvalue.DefaultPlainAttrValidationManager; import org.apache.syncope.core.persistence.common.content.KeymasterConfParamLoader; @@ -48,8 +49,14 @@ public class CommonPersistenceContext { @ConditionalOnMissingBean @Bean - public SearchCondVisitor searchCondVisitor() { - return new SearchCondVisitor(); + public AnySearchCondVisitor anySearchCondVisitor() { + return new AnySearchCondVisitor(); + } + + @ConditionalOnMissingBean + @Bean + public RealmSearchCondVisitor realmSearchCondVisitor() { + return new RealmSearchCondVisitor(); } @Bean diff --git a/core/persistence-common/src/main/java/org/apache/syncope/core/persistence/common/dao/AbstractAnySearchDAO.java b/core/persistence-common/src/main/java/org/apache/syncope/core/persistence/common/dao/AbstractAnySearchDAO.java index c3c1d89e35e..185a19831cb 100644 --- a/core/persistence-common/src/main/java/org/apache/syncope/core/persistence/common/dao/AbstractAnySearchDAO.java +++ b/core/persistence-common/src/main/java/org/apache/syncope/core/persistence/common/dao/AbstractAnySearchDAO.java @@ -18,20 +18,13 @@ */ package org.apache.syncope.core.persistence.common.dao; -import jakarta.validation.ValidationException; -import jakarta.validation.constraints.Max; -import jakarta.validation.constraints.Min; -import java.lang.annotation.Annotation; -import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Comparator; import java.util.List; import java.util.Set; import java.util.stream.Collectors; -import org.apache.commons.lang3.ArrayUtils; 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.attrvalue.PlainAttrValidationManager; import org.apache.syncope.core.persistence.api.dao.AnyObjectDAO; import org.apache.syncope.core.persistence.api.dao.AnySearchDAO; @@ -49,11 +42,8 @@ import org.apache.syncope.core.persistence.api.dao.search.RelationshipCond; 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.AnyUtilsFactory; import org.apache.syncope.core.persistence.api.entity.EntityFactory; -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.persistence.api.entity.anyobject.AnyObject; import org.slf4j.Logger; @@ -61,23 +51,17 @@ import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; +import org.springframework.transaction.annotation.Transactional; import org.springframework.util.CollectionUtils; -public abstract class AbstractAnySearchDAO implements AnySearchDAO { - - protected record CheckResult(PlainSchema schema, PlainAttrValue value, C cond) { - - } +public abstract class AbstractAnySearchDAO extends AbstractSearchDAO implements AnySearchDAO { protected static final Logger LOG = LoggerFactory.getLogger(AnySearchDAO.class); - protected static final String ALWAYS_FALSE_CLAUSE = "1=2"; - - private static final String[] ORDER_BY_NOT_ALLOWED = { - "serialVersionUID", "password", "securityQuestion", "securityAnswer", "token", "tokenExpireTime" - }; + private static final Set ORDER_BY_NOT_ALLOWED = Set.of( + "serialVersionUID", "password", "securityQuestion", "securityAnswer", "token", "tokenExpireTime"); - protected static final String[] RELATIONSHIP_FIELDS = { "realm", "userOwner", "groupOwner" }; + protected static final Set RELATIONSHIP_FIELDS = Set.of("realm", "userOwner", "groupOwner"); protected static SearchCond buildEffectiveCond( final SearchCond cond, @@ -118,36 +102,6 @@ protected static SearchCond buildEffectiveCond( return SearchCond.and(result); } - public static String key(final AttrSchemaType schemaType) { - String key; - 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: - key = "stringValue"; - } - - return key; - } - protected final RealmSearchDAO realmSearchDAO; protected final DynRealmDAO dynRealmDAO; @@ -158,14 +112,8 @@ public static String key(final AttrSchemaType schemaType) { protected final AnyObjectDAO anyObjectDAO; - protected final PlainSchemaDAO plainSchemaDAO; - - protected final EntityFactory entityFactory; - protected final AnyUtilsFactory anyUtilsFactory; - protected final PlainAttrValidationManager validator; - public AbstractAnySearchDAO( final RealmSearchDAO realmSearchDAO, final DynRealmDAO dynRealmDAO, @@ -177,15 +125,28 @@ public AbstractAnySearchDAO( final AnyUtilsFactory anyUtilsFactory, final PlainAttrValidationManager validator) { + super(plainSchemaDAO, entityFactory, validator); this.realmSearchDAO = realmSearchDAO; this.dynRealmDAO = dynRealmDAO; this.userDAO = userDAO; this.groupDAO = groupDAO; this.anyObjectDAO = anyObjectDAO; - this.plainSchemaDAO = plainSchemaDAO; - this.entityFactory = entityFactory; this.anyUtilsFactory = anyUtilsFactory; - this.validator = validator; + } + + @Transactional(readOnly = true) + @Override + public List findByDerAttrValue( + final String expression, + final String value, + final boolean ignoreCaseMatch, + final AnyTypeKind anyTypeKind) { + + List conditions = buildDerAttrValueConditions(expression, value, ignoreCaseMatch); + + LOG.debug("Generated search {} conditions: {}", anyTypeKind, conditions); + + return conditions.isEmpty() ? List.of() : search(SearchCond.and(conditions), anyTypeKind); } protected abstract long doCount( @@ -239,96 +200,6 @@ protected abstract List doSearch( Pageable pageable, AnyTypeKind kind); - protected CheckResult check(final AttrCond cond) { - PlainSchema schema = plainSchemaDAO.findById(cond.getSchema()). - orElseThrow(() -> new IllegalArgumentException("Invalid schema " + cond.getSchema())); - - PlainAttrValue attrValue = new PlainAttrValue(); - - if (AttrSchemaType.Encrypted == schema.getType()) { - throw new IllegalArgumentException("Cannot search by encrypted schema " + cond.getSchema()); - } - - try { - if (cond.getType() != AttrCond.Type.LIKE - && cond.getType() != AttrCond.Type.ILIKE - && cond.getType() != AttrCond.Type.ISNULL - && cond.getType() != AttrCond.Type.ISNOTNULL) { - - validator.validate(schema, cond.getExpression(), attrValue); - } - } catch (ValidationException e) { - throw new IllegalArgumentException("Could not validate expression " + cond.getExpression()); - } - - return new CheckResult<>(schema, attrValue, cond); - } - - protected CheckResult check(final AnyCond cond, final AnyTypeKind kind) { - AnyCond computed = new AnyCond(cond.getType()); - computed.setSchema(cond.getSchema()); - computed.setExpression(cond.getExpression()); - - AnyUtils anyUtils = anyUtilsFactory.getInstance(kind); - - Field anyField = anyUtils.getField(computed.getSchema()). - orElseThrow(() -> new IllegalArgumentException("Invalid schema " + computed.getSchema())); - - // Keeps track of difference between entity's getKey() and JPA @Id fields - if ("key".equals(computed.getSchema())) { - computed.setSchema("id"); - } - - PlainSchema schema = entityFactory.newEntity(PlainSchema.class); - schema.setKey(anyField.getName()); - for (AttrSchemaType attrSchemaType : AttrSchemaType.values()) { - if (anyField.getType().isAssignableFrom(attrSchemaType.getType())) { - schema.setType(attrSchemaType); - } - } - if (schema.getType() == null || schema.getType() == AttrSchemaType.Dropdown) { - schema.setType(AttrSchemaType.String); - } - - // Deal with any Integer fields logically mapping to boolean values - boolean foundBooleanMin = false; - boolean foundBooleanMax = false; - if (Integer.class.equals(anyField.getType())) { - for (Annotation annotation : anyField.getAnnotations()) { - if (Min.class.equals(annotation.annotationType())) { - foundBooleanMin = ((Min) annotation).value() == 0; - } else if (Max.class.equals(annotation.annotationType())) { - foundBooleanMax = ((Max) annotation).value() == 1; - } - } - } - if (foundBooleanMin && foundBooleanMax) { - schema.setType(AttrSchemaType.Boolean); - } - - // Deal with any fields representing relationships to other entities - if (ArrayUtils.contains(RELATIONSHIP_FIELDS, computed.getSchema())) { - computed.setSchema(computed.getSchema() + "_id"); - schema.setType(AttrSchemaType.String); - } - - PlainAttrValue attrValue = new PlainAttrValue(); - if (computed.getType() != AttrCond.Type.LIKE - && computed.getType() != AttrCond.Type.ILIKE - && computed.getType() != AttrCond.Type.ISNULL - && computed.getType() != AttrCond.Type.ISNOTNULL) { - - try { - validator.validate(schema, computed.getExpression(), attrValue); - } catch (ValidationException e) { - LOG.error("Could not validate expression {}", computed.getExpression(), e); - throw new IllegalArgumentException("Could not validate expression " + computed.getExpression()); - } - } - - return new CheckResult<>(schema, attrValue, computed); - } - protected boolean isPatternMatch(final String clause) { return clause.indexOf('%') != -1; } @@ -420,7 +291,7 @@ public List search( new Sort.Order(Sort.Direction.ASC, kind == AnyTypeKind.USER ? "username" : "name")); } else { effectiveOrderBy = pageable.getSort().stream(). - filter(clause -> !ArrayUtils.contains(ORDER_BY_NOT_ALLOWED, clause.getProperty())). + filter(clause -> !ORDER_BY_NOT_ALLOWED.contains(clause.getProperty())). toList(); } diff --git a/core/persistence-common/src/main/java/org/apache/syncope/core/persistence/common/dao/AbstractRealmSearchDAO.java b/core/persistence-common/src/main/java/org/apache/syncope/core/persistence/common/dao/AbstractRealmSearchDAO.java new file mode 100644 index 00000000000..8cc57b85b52 --- /dev/null +++ b/core/persistence-common/src/main/java/org/apache/syncope/core/persistence/common/dao/AbstractRealmSearchDAO.java @@ -0,0 +1,106 @@ +/* + * 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.common.dao; + +import java.util.List; +import java.util.Set; +import org.apache.syncope.common.lib.SyncopeConstants; +import org.apache.syncope.core.persistence.api.attrvalue.PlainAttrValidationManager; +import org.apache.syncope.core.persistence.api.dao.PlainSchemaDAO; +import org.apache.syncope.core.persistence.api.dao.RealmSearchDAO; +import org.apache.syncope.core.persistence.api.dao.search.SearchCond; +import org.apache.syncope.core.persistence.api.entity.EntityFactory; +import org.apache.syncope.core.persistence.api.entity.Realm; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.transaction.annotation.Transactional; + +public abstract class AbstractRealmSearchDAO extends AbstractSearchDAO implements RealmSearchDAO { + + private static final Set ORDER_BY_NOT_ALLOWED = Set.of( + "serialVersionUID", "parent"); + + protected static final Set RELATIONSHIP_FIELDS = Set.of( + "parent", "passwordPolicy", "accountPolicy", "authPolicy", "accessPolicy", "attrReleasePolicy", + "ticketExpirationPolicy"); + + public AbstractRealmSearchDAO( + final PlainSchemaDAO plainSchemaDAO, + final EntityFactory entityFactory, + final PlainAttrValidationManager validator) { + + super(plainSchemaDAO, entityFactory, validator); + } + + @Transactional(readOnly = true) + @Override + public List findByDerAttrValue( + final String expression, + final String value, + final boolean ignoreCaseMatch) { + + List conditions = buildDerAttrValueConditions(expression, value, ignoreCaseMatch); + + LOG.debug("Generated search Realm conditions: {}", conditions); + + return conditions.isEmpty() ? List.of() : search( + Set.of(SyncopeConstants.ROOT_REALM), SearchCond.and(conditions), Pageable.unpaged()); + } + + protected abstract long doCount(Set bases, SearchCond cond); + + @Override + public long count(final Set bases, final SearchCond cond) { + LOG.debug("Search condition:\n{}", cond); + if (cond == null || !cond.isValid()) { + LOG.error("Invalid search condition:\n{}", cond); + return 0; + } + + return doCount(bases, cond); + } + + protected abstract List doSearch(Set bases, SearchCond cond, Pageable pageable); + + @Override + public List search(final Set bases, final SearchCond cond, final Pageable pageable) { + LOG.debug("Search condition:\n{}", cond); + if (cond == null || !cond.isValid()) { + LOG.error("Invalid search condition:\n{}", cond); + return List.of(); + } + + List effectiveOrderBy; + if (pageable.getSort().isEmpty()) { + effectiveOrderBy = List.of(new Sort.Order(Sort.Direction.ASC, "name")); + } else { + effectiveOrderBy = pageable.getSort().stream(). + filter(clause -> !ORDER_BY_NOT_ALLOWED.contains(clause.getProperty())). + toList(); + } + + return doSearch( + bases, + cond, + pageable.isUnpaged() + ? Pageable.unpaged(Sort.by(effectiveOrderBy)) + : PageRequest.of(pageable.getPageNumber(), pageable.getPageSize(), Sort.by(effectiveOrderBy))); + } +} diff --git a/core/persistence-common/src/main/java/org/apache/syncope/core/persistence/common/dao/AbstractSearchDAO.java b/core/persistence-common/src/main/java/org/apache/syncope/core/persistence/common/dao/AbstractSearchDAO.java new file mode 100644 index 00000000000..02c87b9c4e2 --- /dev/null +++ b/core/persistence-common/src/main/java/org/apache/syncope/core/persistence/common/dao/AbstractSearchDAO.java @@ -0,0 +1,288 @@ +/* + * 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.common.dao; + +import jakarta.validation.ValidationException; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Supplier; +import java.util.regex.Pattern; +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.common.lib.SyncopeClientException; +import org.apache.syncope.common.lib.types.AttrSchemaType; +import org.apache.syncope.common.lib.types.ClientExceptionType; +import org.apache.syncope.core.persistence.api.attrvalue.PlainAttrValidationManager; +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.EntityFactory; +import org.apache.syncope.core.persistence.api.entity.PlainAttrValue; +import org.apache.syncope.core.persistence.api.entity.PlainSchema; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public abstract class AbstractSearchDAO { + + public record CheckResult(PlainSchema schema, PlainAttrValue value, C cond) { + + } + + protected static final Logger LOG = LoggerFactory.getLogger(AbstractSearchDAO.class); + + protected static final String ALWAYS_FALSE_CLAUSE = "1=2"; + + public static String key(final AttrSchemaType schemaType) { + return switch (schemaType) { + case Boolean -> + "booleanValue"; + + case Date -> + "dateValue"; + + case Double -> + "doubleValue"; + + case Long -> + "longValue"; + + case Binary -> + "binaryValue"; + + default -> + "stringValue"; + }; + } + + protected static final Comparator LITERAL_COMPARATOR = (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 an attribute value recurring on provided literals/tokens. + * + * @param attrValue value to be split + * @param literals literals/tokens + * @return split value + */ + protected static List split(final String attrValue, final List literals) { + final List attrValues = new ArrayList<>(); + + if (literals.isEmpty()) { + attrValues.add(attrValue); + } else { + for (String token : attrValue.split(Pattern.quote(literals.getFirst()))) { + if (!token.isEmpty()) { + attrValues.addAll(split(token, literals.subList(1, literals.size()))); + } + } + } + + return attrValues; + } + + protected static Supplier syncopeClientException(final String message) { + return () -> { + SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.InvalidSearchParameters); + sce.getElements().add(message); + return sce; + }; + } + + protected final PlainSchemaDAO plainSchemaDAO; + + protected final EntityFactory entityFactory; + + protected final PlainAttrValidationManager validator; + + protected AbstractSearchDAO( + final PlainSchemaDAO plainSchemaDAO, + final EntityFactory entityFactory, + final PlainAttrValidationManager validator) { + + this.plainSchemaDAO = plainSchemaDAO; + this.entityFactory = entityFactory; + this.validator = validator; + } + + protected List buildDerAttrValueConditions( + final String expression, + final String value, + final boolean ignoreCaseMatch) { + + Parser parser = new Parser(expression); + + // Schema keys + List identifiers = new ArrayList<>(); + + // Literals + List 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(LITERAL_COMPARATOR); + + // Split value on provided literals + List attrValues = split(value, literals); + + if (attrValues.size() != identifiers.size()) { + LOG.error("Ambiguous JEXL expression resolution: literals and values have different size"); + return List.of(); + } + + List conditions = new ArrayList<>(); + + // Contains used identifiers in order to avoid replications + Set used = new HashSet<>(); + + // Create several clauses: one for eanch identifiers + for (int i = 0; i < identifiers.size() && !used.contains(identifiers.get(i)); i++) { + used.add(identifiers.get(i)); + + AttrCond cond = plainSchemaDAO.findById(identifiers.get(i)). + map(schema -> new AttrCond()). + orElseGet(() -> new AnyCond()); + cond.setType(ignoreCaseMatch ? AttrCond.Type.IEQ : AttrCond.Type.EQ); + cond.setSchema(identifiers.get(i)); + cond.setExpression(attrValues.get(i)); + conditions.add(SearchCond.of(cond)); + } + + return conditions; + } + + protected CheckResult check(final AttrCond cond) { + PlainSchema schema = plainSchemaDAO.findById(cond.getSchema()). + orElseThrow(() -> new IllegalArgumentException("Invalid schema " + cond.getSchema())); + + PlainAttrValue attrValue = new PlainAttrValue(); + + if (AttrSchemaType.Encrypted == schema.getType()) { + throw new IllegalArgumentException("Cannot search by encrypted schema " + cond.getSchema()); + } + + try { + if (cond.getType() != AttrCond.Type.LIKE + && cond.getType() != AttrCond.Type.ILIKE + && cond.getType() != AttrCond.Type.ISNULL + && cond.getType() != AttrCond.Type.ISNOTNULL) { + + validator.validate(schema, cond.getExpression(), attrValue); + } + } catch (ValidationException e) { + throw new IllegalArgumentException("Could not validate expression " + cond.getExpression()); + } + + return new CheckResult<>(schema, attrValue, cond); + } + + protected CheckResult check(final AnyCond cond, final Field field, final Set relationshipsFields) { + AnyCond computed = new AnyCond(cond.getType()); + computed.setSchema(cond.getSchema()); + computed.setExpression(cond.getExpression()); + + // Keeps track of difference between entity's getKey() and @Id fields + if ("key".equals(computed.getSchema())) { + computed.setSchema("id"); + } + + PlainSchema schema = entityFactory.newEntity(PlainSchema.class); + schema.setKey(field.getName()); + for (AttrSchemaType attrSchemaType : AttrSchemaType.values()) { + if (field.getType().isAssignableFrom(attrSchemaType.getType())) { + schema.setType(attrSchemaType); + } + } + if (schema.getType() == null || schema.getType() == AttrSchemaType.Dropdown) { + schema.setType(AttrSchemaType.String); + } + + // Deal with any Integer fields logically mapping to boolean values + boolean foundBooleanMin = false; + boolean foundBooleanMax = false; + if (Integer.class.equals(field.getType())) { + for (Annotation annotation : field.getAnnotations()) { + if (Min.class.equals(annotation.annotationType())) { + foundBooleanMin = ((Min) annotation).value() == 0; + } else if (Max.class.equals(annotation.annotationType())) { + foundBooleanMax = ((Max) annotation).value() == 1; + } + } + } + if (foundBooleanMin && foundBooleanMax) { + schema.setType(AttrSchemaType.Boolean); + } + + // Deal with fields representing relationships to other entities + if (relationshipsFields.contains(computed.getSchema())) { + computed.setSchema(computed.getSchema() + "_id"); + schema.setType(AttrSchemaType.String); + } + + PlainAttrValue attrValue = new PlainAttrValue(); + if (computed.getType() != AttrCond.Type.LIKE + && computed.getType() != AttrCond.Type.ILIKE + && computed.getType() != AttrCond.Type.ISNULL + && computed.getType() != AttrCond.Type.ISNOTNULL) { + + try { + validator.validate(schema, computed.getExpression(), attrValue); + } catch (ValidationException e) { + LOG.error("Could not validate expression {}", computed.getExpression(), e); + throw new IllegalArgumentException("Could not validate expression " + computed.getExpression()); + } + } + + return new CheckResult<>(schema, attrValue, computed); + } +} diff --git a/core/persistence-common/src/main/java/org/apache/syncope/core/persistence/common/dao/AnyFinder.java b/core/persistence-common/src/main/java/org/apache/syncope/core/persistence/common/dao/AnyFinder.java deleted file mode 100644 index 4706ddd1a05..00000000000 --- a/core/persistence-common/src/main/java/org/apache/syncope/core/persistence/common/dao/AnyFinder.java +++ /dev/null @@ -1,155 +0,0 @@ -/* - * 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.common.dao; - -import java.util.ArrayList; -import java.util.Comparator; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.regex.Pattern; -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.common.lib.types.AnyTypeKind; -import org.apache.syncope.core.persistence.api.dao.AnySearchDAO; -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.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.transaction.annotation.Transactional; - -public class AnyFinder { - - protected static final Logger LOG = LoggerFactory.getLogger(AnyFinder.class); - - protected static final Comparator LITERAL_COMPARATOR = (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 an attribute value recurring on provided literals/tokens. - * - * @param attrValue value to be split - * @param literals literals/tokens - * @return split value - */ - protected static List split(final String attrValue, final List literals) { - final List attrValues = new ArrayList<>(); - - if (literals.isEmpty()) { - attrValues.add(attrValue); - } else { - for (String token : attrValue.split(Pattern.quote(literals.getFirst()))) { - if (!token.isEmpty()) { - attrValues.addAll(split(token, literals.subList(1, literals.size()))); - } - } - } - - return attrValues; - } - - protected final PlainSchemaDAO plainSchemaDAO; - - protected final AnySearchDAO anySearchDAO; - - public AnyFinder(final PlainSchemaDAO plainSchemaDAO, final AnySearchDAO anySearchDAO) { - this.plainSchemaDAO = plainSchemaDAO; - this.anySearchDAO = anySearchDAO; - } - - @Transactional(readOnly = true) - public List findByDerAttrValue( - final AnyTypeKind anyTypeKind, - final String expression, - final String value, - final boolean ignoreCaseMatch) { - - Parser parser = new Parser(expression); - - // Schema keys - List identifiers = new ArrayList<>(); - - // Literals - List 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(LITERAL_COMPARATOR); - - // Split value on provided literals - List attrValues = split(value, literals); - - if (attrValues.size() != identifiers.size()) { - LOG.error("Ambiguous JEXL expression resolution: literals and values have different size"); - return List.of(); - } - - List andConditions = new ArrayList<>(); - - // Contains used identifiers in order to avoid replications - Set used = new HashSet<>(); - - // Create several clauses: one for eanch identifiers - for (int i = 0; i < identifiers.size() && !used.contains(identifiers.get(i)); i++) { - used.add(identifiers.get(i)); - - AttrCond cond = plainSchemaDAO.findById(identifiers.get(i)). - map(schema -> new AttrCond()). - orElseGet(() -> new AnyCond()); - cond.setType(ignoreCaseMatch ? AttrCond.Type.IEQ : AttrCond.Type.EQ); - cond.setSchema(identifiers.get(i)); - cond.setExpression(attrValues.get(i)); - andConditions.add(SearchCond.of(cond)); - } - - LOG.debug("Generated search {} conditions: {}", anyTypeKind, andConditions); - - return anySearchDAO.search(SearchCond.and(andConditions), anyTypeKind); - } -} diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/MariaDBPersistenceContext.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/MariaDBPersistenceContext.java index 4af10b53c39..6d746bc8770 100644 --- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/MariaDBPersistenceContext.java +++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/MariaDBPersistenceContext.java @@ -19,7 +19,6 @@ package org.apache.syncope.core.persistence.jpa; import jakarta.persistence.EntityManager; -import jakarta.persistence.EntityManagerFactory; import org.apache.syncope.core.persistence.api.attrvalue.PlainAttrValidationManager; import org.apache.syncope.core.persistence.api.dao.AnyObjectDAO; import org.apache.syncope.core.persistence.api.dao.AnySearchDAO; @@ -31,7 +30,9 @@ import org.apache.syncope.core.persistence.api.dao.UserDAO; import org.apache.syncope.core.persistence.api.entity.AnyUtilsFactory; import org.apache.syncope.core.persistence.api.entity.EntityFactory; +import org.apache.syncope.core.persistence.api.utils.RealmUtils; import org.apache.syncope.core.persistence.jpa.dao.MariaDBJPAAnySearchDAO; +import org.apache.syncope.core.persistence.jpa.dao.MariaDBJPARealmSearchDAO; import org.apache.syncope.core.persistence.jpa.dao.repo.MariaDBPlainSchemaRepoExtImpl; import org.apache.syncope.core.persistence.jpa.dao.repo.PlainSchemaRepoExt; import org.apache.syncope.core.persistence.jpa.entity.MariaDBEntityFactory; @@ -64,7 +65,6 @@ public AnySearchDAO anySearchDAO( final @Lazy EntityFactory entityFactory, final AnyUtilsFactory anyUtilsFactory, final PlainAttrValidationManager validator, - final EntityManagerFactory entityManagerFactory, final EntityManager entityManager) { return new MariaDBJPAAnySearchDAO( @@ -77,10 +77,26 @@ public AnySearchDAO anySearchDAO( entityFactory, anyUtilsFactory, validator, - entityManagerFactory, entityManager); } + @ConditionalOnMissingBean + @Bean + public RealmSearchDAO realmSearchDAO( + final EntityManager entityManager, + final @Lazy PlainSchemaDAO plainSchemaDAO, + final @Lazy EntityFactory entityFactory, + final PlainAttrValidationManager validator, + final RealmUtils realmUtils) { + + return new MariaDBJPARealmSearchDAO( + entityManager, + plainSchemaDAO, + entityFactory, + validator, + realmUtils); + } + @ConditionalOnMissingBean @Bean public PlainSchemaRepoExt plainSchemaRepoExt( diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/MySQLPersistenceContext.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/MySQLPersistenceContext.java index 2a3b61b3227..cdfe41798a3 100644 --- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/MySQLPersistenceContext.java +++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/MySQLPersistenceContext.java @@ -19,7 +19,6 @@ package org.apache.syncope.core.persistence.jpa; import jakarta.persistence.EntityManager; -import jakarta.persistence.EntityManagerFactory; import org.apache.syncope.core.persistence.api.attrvalue.PlainAttrValidationManager; import org.apache.syncope.core.persistence.api.dao.AnyObjectDAO; import org.apache.syncope.core.persistence.api.dao.AnySearchDAO; @@ -31,7 +30,9 @@ import org.apache.syncope.core.persistence.api.dao.UserDAO; import org.apache.syncope.core.persistence.api.entity.AnyUtilsFactory; import org.apache.syncope.core.persistence.api.entity.EntityFactory; +import org.apache.syncope.core.persistence.api.utils.RealmUtils; import org.apache.syncope.core.persistence.jpa.dao.MySQLJPAAnySearchDAO; +import org.apache.syncope.core.persistence.jpa.dao.MySQLJPARealmSearchDAO; import org.apache.syncope.core.persistence.jpa.dao.repo.MySQLPlainSchemaRepoExtImpl; import org.apache.syncope.core.persistence.jpa.dao.repo.PlainSchemaRepoExt; import org.apache.syncope.core.persistence.jpa.entity.MySQLEntityFactory; @@ -64,7 +65,6 @@ public AnySearchDAO anySearchDAO( final @Lazy EntityFactory entityFactory, final AnyUtilsFactory anyUtilsFactory, final PlainAttrValidationManager validator, - final EntityManagerFactory entityManagerFactory, final EntityManager entityManager) { return new MySQLJPAAnySearchDAO( @@ -77,10 +77,26 @@ public AnySearchDAO anySearchDAO( entityFactory, anyUtilsFactory, validator, - entityManagerFactory, entityManager); } + @ConditionalOnMissingBean + @Bean + public RealmSearchDAO realmSearchDAO( + final EntityManager entityManager, + final @Lazy PlainSchemaDAO plainSchemaDAO, + final @Lazy EntityFactory entityFactory, + final PlainAttrValidationManager validator, + final RealmUtils realmUtils) { + + return new MySQLJPARealmSearchDAO( + entityManager, + plainSchemaDAO, + entityFactory, + validator, + realmUtils); + } + @ConditionalOnMissingBean @Bean public PlainSchemaRepoExt plainSchemaRepoExt( diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/OraclePersistenceContext.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/OraclePersistenceContext.java index 89b284e5977..c677d07869b 100644 --- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/OraclePersistenceContext.java +++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/OraclePersistenceContext.java @@ -19,7 +19,6 @@ package org.apache.syncope.core.persistence.jpa; import jakarta.persistence.EntityManager; -import jakarta.persistence.EntityManagerFactory; import org.apache.syncope.core.persistence.api.attrvalue.PlainAttrValidationManager; import org.apache.syncope.core.persistence.api.dao.AnyObjectDAO; import org.apache.syncope.core.persistence.api.dao.AnySearchDAO; @@ -31,7 +30,9 @@ import org.apache.syncope.core.persistence.api.dao.UserDAO; import org.apache.syncope.core.persistence.api.entity.AnyUtilsFactory; import org.apache.syncope.core.persistence.api.entity.EntityFactory; +import org.apache.syncope.core.persistence.api.utils.RealmUtils; import org.apache.syncope.core.persistence.jpa.dao.OracleJPAAnySearchDAO; +import org.apache.syncope.core.persistence.jpa.dao.OracleJPARealmSearchDAO; import org.apache.syncope.core.persistence.jpa.dao.repo.OraclePlainSchemaRepoExtImpl; import org.apache.syncope.core.persistence.jpa.dao.repo.PlainSchemaRepoExt; import org.apache.syncope.core.persistence.jpa.entity.OracleEntityFactory; @@ -64,7 +65,6 @@ public AnySearchDAO anySearchDAO( final @Lazy EntityFactory entityFactory, final AnyUtilsFactory anyUtilsFactory, final PlainAttrValidationManager validator, - final EntityManagerFactory entityManagerFactory, final EntityManager entityManager) { return new OracleJPAAnySearchDAO( @@ -77,10 +77,26 @@ public AnySearchDAO anySearchDAO( entityFactory, anyUtilsFactory, validator, - entityManagerFactory, entityManager); } + @ConditionalOnMissingBean + @Bean + public RealmSearchDAO realmSearchDAO( + final EntityManager entityManager, + final @Lazy PlainSchemaDAO plainSchemaDAO, + final @Lazy EntityFactory entityFactory, + final PlainAttrValidationManager validator, + final RealmUtils realmUtils) { + + return new OracleJPARealmSearchDAO( + entityManager, + plainSchemaDAO, + entityFactory, + validator, + realmUtils); + } + @ConditionalOnMissingBean @Bean public PlainSchemaRepoExt plainSchemaRepoExt( diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/PGPersistenceContext.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/PGPersistenceContext.java index 6b3640ae1bf..caa7280eee4 100644 --- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/PGPersistenceContext.java +++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/PGPersistenceContext.java @@ -19,7 +19,6 @@ package org.apache.syncope.core.persistence.jpa; import jakarta.persistence.EntityManager; -import jakarta.persistence.EntityManagerFactory; import org.apache.syncope.core.persistence.api.attrvalue.PlainAttrValidationManager; import org.apache.syncope.core.persistence.api.dao.AnyObjectDAO; import org.apache.syncope.core.persistence.api.dao.AnySearchDAO; @@ -31,7 +30,9 @@ import org.apache.syncope.core.persistence.api.dao.UserDAO; import org.apache.syncope.core.persistence.api.entity.AnyUtilsFactory; import org.apache.syncope.core.persistence.api.entity.EntityFactory; +import org.apache.syncope.core.persistence.api.utils.RealmUtils; import org.apache.syncope.core.persistence.jpa.dao.PGJPAAnySearchDAO; +import org.apache.syncope.core.persistence.jpa.dao.PGJPARealmSearchDAO; import org.apache.syncope.core.persistence.jpa.dao.repo.PGPlainSchemaRepoExtImpl; import org.apache.syncope.core.persistence.jpa.dao.repo.PlainSchemaRepoExt; import org.apache.syncope.core.persistence.jpa.entity.PGEntityFactory; @@ -65,7 +66,6 @@ public AnySearchDAO anySearchDAO( final @Lazy EntityFactory entityFactory, final AnyUtilsFactory anyUtilsFactory, final PlainAttrValidationManager validator, - final EntityManagerFactory entityManagerFactory, final EntityManager entityManager) { return new PGJPAAnySearchDAO( @@ -78,10 +78,21 @@ public AnySearchDAO anySearchDAO( entityFactory, anyUtilsFactory, validator, - entityManagerFactory, entityManager); } + @ConditionalOnMissingBean + @Bean + public RealmSearchDAO realmSearchDAO( + final EntityManager entityManager, + final @Lazy PlainSchemaDAO plainSchemaDAO, + final @Lazy EntityFactory entityFactory, + final PlainAttrValidationManager validator, + final RealmUtils realmUtils) { + + return new PGJPARealmSearchDAO(entityManager, plainSchemaDAO, entityFactory, validator, realmUtils); + } + @ConditionalOnMissingBean @Bean public PlainSchemaRepoExt plainSchemaRepoExt( diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/PersistenceContext.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/PersistenceContext.java index 3545e6aacaa..2a5bf95a5bf 100644 --- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/PersistenceContext.java +++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/PersistenceContext.java @@ -81,10 +81,9 @@ import org.apache.syncope.core.persistence.api.entity.AnyUtilsFactory; import org.apache.syncope.core.persistence.api.entity.EntityFactory; import org.apache.syncope.core.persistence.api.entity.task.TaskUtilsFactory; -import org.apache.syncope.core.persistence.api.search.SearchCondVisitor; +import org.apache.syncope.core.persistence.api.search.AnySearchCondVisitor; import org.apache.syncope.core.persistence.common.CommonPersistenceContext; import org.apache.syncope.core.persistence.common.RuntimeDomainLoader; -import org.apache.syncope.core.persistence.common.dao.AnyFinder; import org.apache.syncope.core.persistence.jpa.content.XMLContentExporter; import org.apache.syncope.core.persistence.jpa.content.XMLContentLoader; import org.apache.syncope.core.persistence.jpa.dao.JPAAnyMatchDAO; @@ -96,7 +95,6 @@ import org.apache.syncope.core.persistence.jpa.dao.JPAPersistenceInfoDAO; import org.apache.syncope.core.persistence.jpa.dao.JPAPolicyDAO; import org.apache.syncope.core.persistence.jpa.dao.JPARealmDAO; -import org.apache.syncope.core.persistence.jpa.dao.JPARealmSearchDAO; import org.apache.syncope.core.persistence.jpa.dao.JPATaskDAO; import org.apache.syncope.core.persistence.jpa.dao.JPATaskExecDAO; import org.apache.syncope.core.persistence.jpa.dao.repo.AccessTokenRepo; @@ -361,12 +359,6 @@ protected Class getRepositoryBaseClass(final RepositoryMetadata metadata) { }; } - @ConditionalOnMissingBean - @Bean - public AnyFinder anyFinder(final @Lazy PlainSchemaDAO plainSchemaDAO, final @Lazy AnySearchDAO anySearchDAO) { - return new AnyFinder(plainSchemaDAO, anySearchDAO); - } - @ConditionalOnMissingBean @Bean public AccessTokenDAO accessTokenDAO(final JpaRepositoryFactory jpaRepositoryFactory) { @@ -404,8 +396,7 @@ public AnyObjectRepoExt anyObjectRepoExt( final @Lazy PlainSchemaDAO plainSchemaDAO, final @Lazy UserDAO userDAO, final @Lazy GroupDAO groupDAO, - final EntityManager entityManager, - final AnyFinder anyFinder) { + final EntityManager entityManager) { return new AnyObjectRepoExtImpl( anyUtilsFactory, @@ -413,8 +404,7 @@ public AnyObjectRepoExt anyObjectRepoExt( plainSchemaDAO, userDAO, groupDAO, - entityManager, - anyFinder); + entityManager); } @ConditionalOnMissingBean @@ -593,7 +583,7 @@ public DynRealmRepoExt dynRealmRepoExt( final @Lazy AnyObjectDAO anyObjectDAO, final AnySearchDAO anySearchDAO, final AnyMatchDAO anyMatchDAO, - final SearchCondVisitor searchCondVisitor, + final AnySearchCondVisitor searchCondVisitor, final EntityManager entityManager) { return new DynRealmRepoExtImpl( @@ -649,9 +639,8 @@ public GroupRepoExt groupRepoExt( final @Lazy UserDAO userDAO, final @Lazy AnyObjectDAO anyObjectDAO, final AnySearchDAO anySearchDAO, - final SearchCondVisitor searchCondVisitor, - final EntityManager entityManager, - final AnyFinder anyFinder) { + final AnySearchCondVisitor searchCondVisitor, + final EntityManager entityManager) { return new GroupRepoExtImpl( anyUtilsFactory, @@ -664,8 +653,7 @@ public GroupRepoExt groupRepoExt( anyObjectDAO, anySearchDAO, searchCondVisitor, - entityManager, - anyFinder); + entityManager); } @ConditionalOnMissingBean @@ -795,12 +783,6 @@ public RealmDAO realmDAO( return new JPARealmDAO(roleDAO, realmSearchDAO, plainSchemaDAO, publisher, entityManager); } - @ConditionalOnMissingBean - @Bean - public RealmSearchDAO realmSearchDAO(final EntityManager entityManager) { - return new JPARealmSearchDAO(entityManager); - } - @ConditionalOnMissingBean @Bean public RelationshipTypeRepoExt relationshipTypeRepoExt(final EntityManager entityManager) { @@ -887,7 +869,7 @@ public RoleRepoExt roleRepoExt( final @Lazy AnyMatchDAO anyMatchDAO, final @Lazy AnySearchDAO anySearchDAO, final DelegationDAO delegationDAO, - final SearchCondVisitor searchCondVisitor, + final AnySearchCondVisitor searchCondVisitor, final EntityManager entityManager) { return new RoleRepoExtImpl( @@ -987,8 +969,7 @@ public UserRepoExt userRepoExt( final @Lazy GroupDAO groupDAO, final DelegationDAO delegationDAO, final FIQLQueryDAO fiqlQueryDAO, - final EntityManager entityManager, - final AnyFinder anyFinder) { + final EntityManager entityManager) { return new UserRepoExtImpl( anyUtilsFactory, @@ -1000,8 +981,7 @@ public UserRepoExt userRepoExt( delegationDAO, fiqlQueryDAO, securityProperties, - entityManager, - anyFinder); + entityManager); } @ConditionalOnMissingBean diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/content/XMLContentExporter.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/content/XMLContentExporter.java index 1dfb232ef12..f271f97bb61 100644 --- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/content/XMLContentExporter.java +++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/content/XMLContentExporter.java @@ -77,7 +77,6 @@ import org.apache.syncope.core.persistence.jpa.entity.JPAAuditEvent; import org.apache.syncope.core.persistence.jpa.entity.JPAJobStatus; import org.apache.syncope.core.persistence.jpa.entity.JPARealm; -import org.springframework.data.domain.Pageable; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.datasource.DataSourceUtils; import org.springframework.jdbc.support.JdbcUtils; @@ -356,7 +355,7 @@ protected void exportTable( if (tableName.equalsIgnoreCase(JPARealm.TABLE)) { List> realmRows = new ArrayList<>(rows); rows.clear(); - realmSearchDAO.findDescendants(SyncopeConstants.ROOT_REALM, null, Pageable.unpaged()). + realmSearchDAO.findDescendants(SyncopeConstants.ROOT_REALM, null). forEach(realm -> realmRows.stream().filter(row -> { String id = Optional.ofNullable(row.get("ID")).orElseGet(() -> row.get("id")); diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/AbstractJPAAnySearchDAO.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/AbstractJPAAnySearchDAO.java index 8b700b82b64..231052dea33 100644 --- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/AbstractJPAAnySearchDAO.java +++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/AbstractJPAAnySearchDAO.java @@ -19,7 +19,6 @@ package org.apache.syncope.core.persistence.jpa.dao; import jakarta.persistence.EntityManager; -import jakarta.persistence.EntityManagerFactory; import jakarta.persistence.Query; import java.util.ArrayList; import java.util.HashMap; @@ -28,14 +27,8 @@ import java.util.Map; import java.util.Optional; import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.function.Supplier; import java.util.stream.Collectors; -import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.StringUtils; -import org.apache.openjpa.jdbc.meta.MappingRepository; -import org.apache.openjpa.jdbc.sql.OracleDictionary; -import org.apache.openjpa.persistence.OpenJPAEntityManagerFactorySPI; import org.apache.syncope.common.lib.SyncopeClientException; import org.apache.syncope.common.lib.SyncopeConstants; import org.apache.syncope.common.lib.types.AnyTypeKind; @@ -70,7 +63,6 @@ import org.apache.syncope.core.persistence.api.entity.Realm; import org.apache.syncope.core.persistence.api.utils.RealmUtils; import org.apache.syncope.core.persistence.common.dao.AbstractAnySearchDAO; -import org.apache.syncope.core.spring.security.AuthContextUtils; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; @@ -96,8 +88,6 @@ protected record AttrCondQuery(Boolean addPlainSchemas, AnySearchNode node) { + "lastChangeDate,lastModifier,status,changePwdDate,cipherAlgorithm,failedLogins," + "lastLoginDate,mustChangePassword,suspended,username"; - private static final Map IS_ORACLE = new ConcurrentHashMap<>(); - protected static int setParameter(final List parameters, final Object parameter) { parameters.add(parameter); return parameters.size(); @@ -113,16 +103,6 @@ protected static void fillWithParameters(final Query query, final List p } } - protected static Supplier syncopeClientException(final String message) { - return () -> { - SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.InvalidSearchParameters); - sce.getElements().add(message); - return sce; - }; - } - - protected final EntityManagerFactory entityManagerFactory; - protected final EntityManager entityManager; protected AbstractJPAAnySearchDAO( @@ -135,7 +115,6 @@ protected AbstractJPAAnySearchDAO( final EntityFactory entityFactory, final AnyUtilsFactory anyUtilsFactory, final PlainAttrValidationManager validator, - final EntityManagerFactory entityManagerFactory, final EntityManager entityManager) { super( @@ -148,21 +127,9 @@ protected AbstractJPAAnySearchDAO( entityFactory, anyUtilsFactory, validator); - this.entityManagerFactory = entityManagerFactory; this.entityManager = entityManager; } - protected boolean isOracle() { - return IS_ORACLE.computeIfAbsent( - AuthContextUtils.getDomain(), - k -> { - OpenJPAEntityManagerFactorySPI emfspi = entityManagerFactory.unwrap( - OpenJPAEntityManagerFactorySPI.class); - return ((MappingRepository) emfspi.getConfiguration(). - getMetaDataRepositoryInstance()).getDBDictionary() instanceof OracleDictionary; - }); - } - protected SearchSupport.SearchView defaultSV(final SearchSupport svs) { return svs.field(); } @@ -562,7 +529,7 @@ protected AnySearchNode.Leaf fillAttrQuery( final boolean not, final List parameters) { - // activate ignoreCase only for EQ and LIKE operators + // activate ignoreCase only for ILIKE and IEQ operators boolean ignoreCase = AttrCond.Type.ILIKE == cond.getType() || AttrCond.Type.IEQ == cond.getType(); String left = column; @@ -585,7 +552,7 @@ protected AnySearchNode.Leaf fillAttrQuery( } else { clause.append('?').append(setParameter(parameters, cond.getExpression())); } - if (isOracle()) { + if (this instanceof OracleJPAAnySearchDAO) { clause.append(" ESCAPE '\\'"); } } else { @@ -655,74 +622,12 @@ protected AnySearchNode.Leaf fillAttrQuery( : from.alias() + ".schema_id='" + schema.getKey() + "' AND " + clause); } - protected AttrCondQuery getQuery( - final AttrCond cond, - final boolean not, - final CheckResult checked, - final List parameters, - final SearchSupport svs) { - - // normalize NULL / NOT NULL checks - if (not) { - if (cond.getType() == AttrCond.Type.ISNULL) { - cond.setType(AttrCond.Type.ISNOTNULL); - } else if (cond.getType() == AttrCond.Type.ISNOTNULL) { - cond.setType(AttrCond.Type.ISNULL); - } - } - - SearchSupport.SearchView sv = checked.schema().isUniqueConstraint() - ? svs.asSearchViewSupport().uniqueAttr() - : svs.asSearchViewSupport().attr(); - - switch (cond.getType()) { - case ISNOTNULL -> { - return new AttrCondQuery(true, new AnySearchNode.Leaf( - sv, - sv.alias() + ".schema_id='" + checked.schema().getKey() + "'")); - } - - case ISNULL -> { - String clause = new StringBuilder(anyId(svs)).append(" NOT IN "). - append('('). - append("SELECT DISTINCT any_id FROM "). - append(sv.name()). - append(" WHERE schema_id=").append("'").append(checked.schema().getKey()).append("'"). - append(')').toString(); - return new AttrCondQuery(true, new AnySearchNode.Leaf(defaultSV(svs), clause)); - } - - default -> { - AnySearchNode.Leaf node; - if (not && checked.schema().isMultivalue()) { - AnySearchNode.Leaf notNode = fillAttrQuery( - sv.alias() + "." + key(checked.schema().getType()), - sv, - checked.value(), - checked.schema(), - cond, - false, - parameters); - node = new AnySearchNode.Leaf( - sv, - anyId(svs) + " NOT IN (" - + "SELECT any_id FROM " + sv.name() - + " WHERE " + notNode.getClause().replace(sv.alias() + ".", "") - + ")"); - } else { - node = fillAttrQuery( - sv.alias() + "." + key(checked.schema().getType()), - sv, - checked.value(), - checked.schema(), - cond, - not, - parameters); - } - return new AttrCondQuery(true, node); - } - } - } + protected abstract AttrCondQuery getQuery( + AttrCond cond, + boolean not, + CheckResult checked, + List parameters, + SearchSupport svs); protected AnySearchNode getQuery( final AnyCond cond, @@ -738,7 +643,11 @@ protected AnySearchNode getQuery( cond.setExpression(realm.getKey()); } - CheckResult checked = check(cond, svs.anyTypeKind); + CheckResult checked = check( + cond, + anyUtilsFactory.getInstance(svs.anyTypeKind).getField(cond.getSchema()). + orElseThrow(() -> new IllegalArgumentException("Invalid schema " + cond.getSchema())), + RELATIONSHIP_FIELDS); return switch (checked.cond().getType()) { case ISNULL -> @@ -801,7 +710,8 @@ protected AdminRealmsFilter getAdminRealmsFilter( return noRealm; }); - realmKeys.addAll(realmSearchDAO.findDescendants(realm.getFullPath(), base.getFullPath())); + realmKeys.addAll(realmSearchDAO.findDescendants(realm.getFullPath(), base.getFullPath()). + stream().map(Realm::getKey).toList()); } else { dynRealmDAO.findById(realmPath).ifPresentOrElse( dynRealm -> dynRealmKeys.add(dynRealm.getKey()), @@ -877,7 +787,7 @@ protected String buildFrom( protected String buildWhere(final List where, final AnySearchNode root) { return where.stream(). map(w -> "(" + w + ")"). - collect(Collectors.joining(" " + root.getType().name() + " ")); + collect(Collectors.joining(' ' + root.getType().name() + ' ')); } protected String buildCountQuery( @@ -900,15 +810,14 @@ protected String buildCountQuery( Map counters = new HashMap<>(); visitNode(root, counters, from, where, svs); - StringBuilder queryString = new StringBuilder("SELECT COUNT(DISTINCT ").append(anyId(svs)).append(") "); - - queryString.append("FROM ").append(buildFrom(from, queryInfo.plainSchemas(), null)); - - queryString.append(" WHERE ").append(buildWhere(where, root)); + String queryString = new StringBuilder("SELECT COUNT(DISTINCT ").append(anyId(svs)).append(") "). + append("FROM ").append(buildFrom(from, queryInfo.plainSchemas(), null)). + append(" WHERE ").append(buildWhere(where, root)). + toString(); LOG.debug("Query: {}, parameters: {}", queryString, parameters); - return queryString.toString(); + return queryString; } @Override @@ -946,40 +855,13 @@ protected long doCount( return ((Number) countQuery.getSingleResult()).intValue(); } - protected void parseOrderByForPlainSchema( - final SearchSupport svs, - final OrderBySupport obs, - final OrderBySupport.Item item, - final Sort.Order clause, - final PlainSchema schema, - final String fieldName) { - - // keep track of involvement of non-mandatory schemas in the order by clauses - obs.nonMandatorySchemas = !"true".equals(schema.getMandatoryCondition()); - - if (schema.isUniqueConstraint()) { - obs.views.add(svs.asSearchViewSupport().uniqueAttr()); - - item.select = new StringBuilder(). - append(svs.asSearchViewSupport().uniqueAttr().alias()).append('.'). - append(key(schema.getType())). - append(" AS ").append(fieldName).toString(); - item.where = new StringBuilder(). - append(svs.asSearchViewSupport().uniqueAttr().alias()). - append(".schema_id='").append(fieldName).append("'").toString(); - item.orderBy = fieldName + ' ' + clause.getDirection().name(); - } else { - obs.views.add(svs.asSearchViewSupport().attr()); - - item.select = new StringBuilder(). - append(svs.asSearchViewSupport().attr().alias()).append('.').append(key(schema.getType())). - append(" AS ").append(fieldName).toString(); - item.where = new StringBuilder(). - append(svs.asSearchViewSupport().attr().alias()). - append(".schema_id='").append(fieldName).append("'").toString(); - item.orderBy = fieldName + ' ' + clause.getDirection().name(); - } - } + protected abstract void parseOrderByForPlainSchema( + SearchSupport svs, + OrderBySupport obs, + OrderBySupport.Item item, + Sort.Order clause, + PlainSchema schema, + String fieldName); protected void parseOrderByForField( final SearchSupport svs, @@ -1023,7 +905,7 @@ protected OrderBySupport parseOrderBy( String fieldName = "key".equals(clause.getProperty()) ? "id" : clause.getProperty(); // Adjust field name to column name - if (ArrayUtils.contains(RELATIONSHIP_FIELDS, fieldName)) { + if (RELATIONSHIP_FIELDS.contains(fieldName)) { fieldName += "_id"; } diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/AbstractJPARealmSearchDAO.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/AbstractJPARealmSearchDAO.java new file mode 100644 index 00000000000..e581b0e6a5c --- /dev/null +++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/AbstractJPARealmSearchDAO.java @@ -0,0 +1,537 @@ +/* + * 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 jakarta.persistence.EntityManager; +import jakarta.persistence.NoResultException; +import jakarta.persistence.Query; +import jakarta.persistence.TypedQuery; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import org.apache.commons.lang3.StringUtils; +import org.apache.syncope.common.lib.SyncopeConstants; +import org.apache.syncope.common.lib.types.AttrSchemaType; +import org.apache.syncope.core.persistence.api.attrvalue.PlainAttrValidationManager; +import org.apache.syncope.core.persistence.api.dao.MalformedPathException; +import org.apache.syncope.core.persistence.api.dao.PlainSchemaDAO; +import org.apache.syncope.core.persistence.api.dao.RealmDAO; +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.EntityFactory; +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.persistence.api.utils.RealmUtils; +import org.apache.syncope.core.persistence.common.dao.AbstractRealmSearchDAO; +import org.apache.syncope.core.persistence.jpa.entity.JPARealm; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.transaction.annotation.Transactional; + +public abstract class AbstractJPARealmSearchDAO extends AbstractRealmSearchDAO { + + protected record QueryInfo(RealmSearchNode node, Set plainSchemas) { + + } + + protected record AttrCondQuery(Boolean addPlainSchemas, RealmSearchNode node) { + + } + + protected static int setParameter(final List parameters, final Object parameter) { + parameters.add(parameter); + return parameters.size(); + } + + protected static void fillWithParameters(final Query query, final List parameters) { + for (int i = 0; i < parameters.size(); i++) { + if (parameters.get(i) instanceof Boolean aBoolean) { + query.setParameter(i + 1, aBoolean ? 1 : 0); + } else { + query.setParameter(i + 1, parameters.get(i)); + } + } + } + + protected final EntityManager entityManager; + + protected final RealmUtils realmUtils; + + protected AbstractJPARealmSearchDAO( + final EntityManager entityManager, + final PlainSchemaDAO plainSchemaDAO, + final EntityFactory entityFactory, + final PlainAttrValidationManager validator, + final RealmUtils realmUtils) { + + super(plainSchemaDAO, entityFactory, validator); + + this.entityManager = entityManager; + this.realmUtils = realmUtils; + } + + @Transactional(readOnly = true) + @Override + public Optional findByFullPath(final String fullPath) { + if (StringUtils.isBlank(fullPath) + || (!SyncopeConstants.ROOT_REALM.equals(fullPath) + && !RealmDAO.PATH_PATTERN.matcher(fullPath).matches())) { + + throw new MalformedPathException(fullPath); + } + + TypedQuery query = entityManager.createQuery( + "SELECT e FROM " + JPARealm.class.getSimpleName() + " e WHERE e.fullPath=:fullPath", Realm.class); + query.setParameter("fullPath", fullPath); + + Realm result = null; + try { + result = query.getSingleResult(); + } catch (NoResultException e) { + LOG.debug("Realm with fullPath {} not found", fullPath, e); + } + + return Optional.ofNullable(result); + } + + @Override + public List findByName(final String name) { + TypedQuery query = entityManager.createQuery( + "SELECT e FROM " + JPARealm.class.getSimpleName() + " e WHERE e.name=:name", Realm.class); + query.setParameter("name", name); + + return query.getResultList(); + } + + @Override + public List findChildren(final Realm realm) { + TypedQuery query = entityManager.createQuery( + "SELECT e FROM " + JPARealm.class.getSimpleName() + " e WHERE e.parent=:realm", Realm.class); + query.setParameter("realm", realm); + + return query.getResultList(); + } + + @Override + public List findDescendants(final String base, final String prefix) { + List parameters = new ArrayList<>(); + + StringBuilder queryString = new StringBuilder("SELECT e FROM "). + append(JPARealm.class.getSimpleName()). + append(" e WHERE (e.fullPath=?").append(setParameter(parameters, base)). + append(" OR e.fullPath LIKE ?").append(setParameter( + parameters, + SyncopeConstants.ROOT_REALM.equals(base) ? "/%" : base + "/%")). + append(") "); + if (prefix != null) { + queryString.append("AND (e.fullPath=?").append(setParameter(parameters, prefix)). + append(" OR e.fullPath LIKE ?").append(setParameter( + parameters, + SyncopeConstants.ROOT_REALM.equals(prefix) ? "/%" : prefix + "/%")). + append(") "); + } + queryString.append("ORDER BY e.fullPath"); + + TypedQuery query = entityManager.createQuery(queryString.toString(), Realm.class); + + fillWithParameters(query, parameters); + + return query.getResultList(); + } + + // ------------------------------------------ // + protected Optional getQueryForCustomConds( + final SearchCond cond, + final boolean not, + final List parameters) { + + return Optional.empty(); + } + + protected Optional getQuery(final SearchCond cond, final List parameters) { + if (cond == null) { + return Optional.empty(); + } + + boolean not = cond.getType() == SearchCond.Type.NOT_LEAF; + + Optional node = Optional.empty(); + Set plainSchemas = new HashSet<>(); + + switch (cond.getType()) { + case LEAF: + case NOT_LEAF: + node = cond.asLeaf(AnyCond.class). + map(anyCond -> getQuery(anyCond, not, parameters)). + or(() -> cond.asLeaf(AttrCond.class). + map(attrCond -> { + CheckResult checked = check(attrCond); + AttrCondQuery query = getQuery(attrCond, not, checked, parameters); + if (query.addPlainSchemas()) { + plainSchemas.add(checked.schema().getKey()); + } + return query.node(); + })); + + if (node.isEmpty()) { + node = getQueryForCustomConds(cond, not, parameters); + } + break; + + case AND: + RealmSearchNode andNode = new RealmSearchNode(RealmSearchNode.Type.AND); + + getQuery(cond.getLeft(), parameters).ifPresent(left -> { + andNode.add(left.node()); + plainSchemas.addAll(left.plainSchemas()); + }); + + getQuery(cond.getRight(), parameters).ifPresent(right -> { + andNode.add(right.node()); + plainSchemas.addAll(right.plainSchemas()); + }); + + if (!andNode.getChildren().isEmpty()) { + node = Optional.of(andNode); + } + break; + + case OR: + RealmSearchNode orNode = new RealmSearchNode(RealmSearchNode.Type.OR); + + getQuery(cond.getLeft(), parameters).ifPresent(left -> { + orNode.add(left.node()); + plainSchemas.addAll(left.plainSchemas()); + }); + + getQuery(cond.getRight(), parameters).ifPresent(right -> { + orNode.add(right.node()); + plainSchemas.addAll(right.plainSchemas()); + }); + + if (!orNode.getChildren().isEmpty()) { + node = Optional.of(orNode); + } + break; + + default: + } + + return node.map(n -> new QueryInfo(n, plainSchemas)); + } + + protected abstract AttrCondQuery getQuery( + AttrCond cond, + boolean not, + CheckResult checked, + List parameters); + + protected RealmSearchNode getQuery(final AnyCond cond, final boolean not, final List parameters) { + CheckResult checked = check( + cond, + realmUtils.getField(cond.getSchema()). + orElseThrow(() -> new IllegalArgumentException("Invalid schema " + cond.getSchema())), + RELATIONSHIP_FIELDS); + + return switch (checked.cond().getType()) { + case ISNULL -> + new RealmSearchNode.Leaf("r." + checked.cond().getSchema() + (not ? " IS NOT NULL" : " IS NULL")); + + case ISNOTNULL -> + new RealmSearchNode.Leaf("r." + checked.cond().getSchema() + (not ? " IS NULL" : " IS NOT NULL")); + + default -> + fillAttrQuery( + "r." + checked.cond().getSchema(), + checked.value(), + checked.schema(), + checked.cond(), + not, + parameters); + }; + } + + protected RealmSearchNode.Leaf fillAttrQuery( + final String column, + final PlainAttrValue attrValue, + final PlainSchema schema, + final AttrCond cond, + final boolean not, + final List parameters) { + + boolean ignoreCase = AttrCond.Type.ILIKE == cond.getType() || AttrCond.Type.IEQ == cond.getType(); + + String left = column; + if (ignoreCase && (schema.getType() == AttrSchemaType.String || schema.getType() == AttrSchemaType.Enum)) { + left = "LOWER(" + left + ')'; + } + + StringBuilder clause = new StringBuilder(left); + switch (cond.getType()) { + + case ILIKE: + case LIKE: + if (schema.getType() == AttrSchemaType.String || schema.getType() == AttrSchemaType.Enum) { + if (not) { + clause.append(" NOT"); + } + clause.append(" LIKE "); + if (ignoreCase) { + clause.append("LOWER(?").append(setParameter(parameters, cond.getExpression())).append(')'); + } else { + clause.append('?').append(setParameter(parameters, cond.getExpression())); + } + if (this instanceof OracleJPARealmSearchDAO) { + clause.append(" ESCAPE '\\'"); + } + } else { + LOG.error("LIKE is only compatible with string or enum schemas"); + return new RealmSearchNode.Leaf(ALWAYS_FALSE_CLAUSE); + } + break; + + case IEQ: + case EQ: + default: + clause.append(not ? "<>" : "="); + if (ignoreCase + && (schema.getType() == AttrSchemaType.String || schema.getType() == AttrSchemaType.Enum)) { + clause.append("LOWER(?").append(setParameter(parameters, attrValue.getValue())).append(')'); + } else { + clause.append('?').append(setParameter(parameters, attrValue.getValue())); + } + break; + + case GE: + clause.append(not ? "<" : ">="); + clause.append('?').append(setParameter(parameters, attrValue.getValue())); + break; + + case GT: + clause.append(not ? "<=" : ">"); + clause.append('?').append(setParameter(parameters, attrValue.getValue())); + break; + + case LE: + clause.append(not ? ">" : "<="); + clause.append('?').append(setParameter(parameters, attrValue.getValue())); + break; + + case LT: + clause.append(not ? ">=" : "<"); + clause.append('?').append(setParameter(parameters, attrValue.getValue())); + break; + } + + return new RealmSearchNode.Leaf( + cond instanceof AnyCond + ? clause.toString() + : "r.schema_id='" + schema.getKey() + "' AND " + clause); + } + + protected void visitNode(final RealmSearchNode node, final List where) { + node.asLeaf().ifPresentOrElse( + leaf -> where.add(leaf.getClause()), + () -> { + List nodeWhere = new ArrayList<>(); + node.getChildren().forEach(child -> visitNode(child, nodeWhere)); + String op = " " + node.getType().name() + " "; + where.add(nodeWhere.stream(). + map(w -> w.contains(" AND ") || w.contains(" OR ") ? "(" + w + ")" : w). + collect(Collectors.joining(op))); + }); + } + + protected String buildFrom(final Set plainSchemas, final OrderBySupport obs) { + return JPARealm.TABLE + " r"; + } + + protected String buildWhere(final Set bases, final QueryInfo queryInfo, final List parameters) { + String fullPaths = bases.stream(). + map(base -> "r.fullPath=?" + setParameter(parameters, base) + + " OR r.fullPath LIKE ?" + setParameter( + parameters, SyncopeConstants.ROOT_REALM.equals(base) ? "/%" : base + "/%")). + collect(Collectors.joining(" OR ")); + + RealmSearchNode root; + if (queryInfo.node().getType() == RealmSearchNode.Type.AND) { + root = queryInfo.node(); + } else { + root = new RealmSearchNode(RealmSearchNode.Type.AND); + root.add(queryInfo.node()); + } + + List where = new ArrayList<>(); + visitNode(root, where); + + return "(" + fullPaths + ')' + + " AND (" + where.stream(). + map(w -> w.contains(" AND ") || w.contains(" OR ") ? "(" + w + ")" : w). + collect(Collectors.joining(' ' + root.getType().name() + ' ')) + + ')'; + } + + @Override + protected long doCount(final Set bases, final SearchCond cond) { + List parameters = new ArrayList<>(); + + QueryInfo queryInfo = getQuery(cond, parameters).orElse(null); + if (queryInfo == null) { + LOG.error("Invalid search condition: {}", cond); + return 0; + } + + String queryString = new StringBuilder("SELECT COUNT(DISTINCT r.id)"). + append(" FROM ").append(buildFrom(queryInfo.plainSchemas(), null)). + append(" WHERE ").append(buildWhere(bases, queryInfo, parameters)). + toString(); + + LOG.debug("Query: {}, parameters: {}", queryString, parameters); + + Query query = entityManager.createNativeQuery(queryString); + fillWithParameters(query, parameters); + + return ((Number) query.getSingleResult()).longValue(); + } + + protected abstract void parseOrderByForPlainSchema( + OrderBySupport obs, + OrderBySupport.Item item, + Sort.Order clause, + PlainSchema schema, + String fieldName); + + protected void parseOrderByForField( + final OrderBySupport.Item item, + final String fieldName, + final Sort.Order clause) { + + item.select = "r." + fieldName; + item.where = StringUtils.EMPTY; + item.orderBy = "r." + fieldName + ' ' + clause.getDirection().name(); + } + + protected void parseOrderByForCustom( + final Sort.Order clause, + final OrderBySupport.Item item, + final OrderBySupport obs) { + + // do nothing by default, meant for subclasses + } + + protected OrderBySupport parseOrderBy(final List orderBy) { + OrderBySupport obs = new OrderBySupport(); + + Set orderByUniquePlainSchemas = new HashSet<>(); + Set orderByNonUniquePlainSchemas = new HashSet<>(); + orderBy.forEach(clause -> { + OrderBySupport.Item item = new OrderBySupport.Item(); + + parseOrderByForCustom(clause, item, obs); + + if (item.isEmpty()) { + realmUtils.getField(clause.getProperty()).ifPresentOrElse( + field -> { + // Manage difference among external key attribute and internal JPA @Id + String fieldName = "key".equals(clause.getProperty()) ? "id" : clause.getProperty(); + + // Adjust field name to column name + if (RELATIONSHIP_FIELDS.contains(fieldName)) { + fieldName += "_id"; + } + + parseOrderByForField(item, fieldName, clause); + }, + () -> { + plainSchemaDAO.findById(clause.getProperty()).ifPresent(schema -> { + if (schema.isUniqueConstraint()) { + orderByUniquePlainSchemas.add(schema.getKey()); + } else { + orderByNonUniquePlainSchemas.add(schema.getKey()); + } + if (orderByUniquePlainSchemas.size() > 1 || orderByNonUniquePlainSchemas.size() > 1) { + throw syncopeClientException("Order by more than one attribute is not allowed; " + + "remove one from " + (orderByUniquePlainSchemas.size() > 1 + ? orderByUniquePlainSchemas : orderByNonUniquePlainSchemas)).get(); + } + parseOrderByForPlainSchema(obs, item, clause, schema, clause.getProperty()); + }); + }); + } + + if (item.isEmpty()) { + LOG.warn("Cannot build any valid clause from {}", clause); + } else { + obs.items.add(item); + } + }); + + return obs; + } + + @Override + protected List doSearch(final Set bases, final SearchCond cond, final Pageable pageable) { + List parameters = new ArrayList<>(); + + QueryInfo queryInfo = getQuery(cond, parameters).orElse(null); + if (queryInfo == null) { + LOG.error("Invalid search condition: {}", cond); + return List.of(); + } + + OrderBySupport obs = parseOrderBy(pageable.getSort().toList()); + + StringBuilder queryString = new StringBuilder("SELECT DISTINCT r.id"); + obs.items.forEach(item -> queryString.append(',').append(item.select)); + + queryString.append(" FROM ").append(buildFrom(queryInfo.plainSchemas(), obs)). + append(" WHERE ").append(buildWhere(bases, queryInfo, parameters)). + toString(); + + if (!obs.items.isEmpty()) { + queryString.append(" ORDER BY "). + append(obs.items.stream().map(item -> item.orderBy).collect(Collectors.joining(","))); + } + + LOG.debug("Query: {}, parameters: {}", queryString, parameters); + + Query query = entityManager.createNativeQuery(queryString.toString()); + + if (pageable.isPaged()) { + query.setFirstResult(pageable.getPageSize() * pageable.getPageNumber()); + query.setMaxResults(pageable.getPageSize()); + } + + fillWithParameters(query, parameters); + + @SuppressWarnings("unchecked") + List keys = query.getResultList().stream(). + map(key -> key instanceof Object[] array ? (String) (array)[0] : ((String) key)). + toList(); + + return keys.stream().map(k -> entityManager.find(JPARealm.class, k)). + filter(Objects::nonNull).map(Realm.class::cast).toList(); + } +} diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPARealmDAO.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPARealmDAO.java index 968ab75eec1..bc665b61689 100644 --- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPARealmDAO.java +++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPARealmDAO.java @@ -262,7 +262,7 @@ public void delete(final Realm realm) { return; } - realmSearchDAO.findDescendants(realm.getFullPath(), null, Pageable.unpaged()).forEach(toBeDeleted -> { + realmSearchDAO.findDescendants(realm.getFullPath(), null).forEach(toBeDeleted -> { roleDAO.findByRealms(toBeDeleted).forEach(role -> role.getRealms().remove(toBeDeleted)); toBeDeleted.setParent(null); diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPARealmSearchDAO.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPARealmSearchDAO.java deleted file mode 100644 index d3aef9eaa9a..00000000000 --- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPARealmSearchDAO.java +++ /dev/null @@ -1,190 +0,0 @@ -/* - * 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 jakarta.persistence.EntityManager; -import jakarta.persistence.NoResultException; -import jakarta.persistence.Query; -import jakarta.persistence.TypedQuery; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import java.util.Set; -import java.util.stream.Collectors; -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.Strings; -import org.apache.syncope.common.lib.SyncopeConstants; -import org.apache.syncope.core.persistence.api.dao.MalformedPathException; -import org.apache.syncope.core.persistence.api.dao.RealmDAO; -import org.apache.syncope.core.persistence.api.dao.RealmSearchDAO; -import org.apache.syncope.core.persistence.api.entity.Realm; -import org.apache.syncope.core.persistence.jpa.entity.JPARealm; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.data.domain.Pageable; -import org.springframework.transaction.annotation.Transactional; - -public class JPARealmSearchDAO implements RealmSearchDAO { - - protected static final Logger LOG = LoggerFactory.getLogger(RealmSearchDAO.class); - - protected static int setParameter(final List parameters, final Object parameter) { - parameters.add(parameter); - return parameters.size(); - } - - protected static StringBuilder buildDescendantsQuery( - final Set bases, - final String keyword, - final List parameters) { - - String basesClause = bases.stream(). - map(base -> "e.fullPath=?" + setParameter(parameters, base) - + " OR e.fullPath LIKE ?" + setParameter( - parameters, SyncopeConstants.ROOT_REALM.equals(base) ? "/%" : base + "/%")). - collect(Collectors.joining(" OR ")); - - StringBuilder queryString = new StringBuilder("SELECT e FROM "). - append(JPARealm.class.getSimpleName()).append(" e "). - append("WHERE (").append(basesClause).append(')'); - - if (keyword != null) { - queryString.append(" AND LOWER(e.name) LIKE ?"). - append(setParameter(parameters, "%" + keyword.replaceAll("_", "\\\\_").toLowerCase() + "%")); - } - - return queryString; - } - - protected final EntityManager entityManager; - - public JPARealmSearchDAO(final EntityManager entityManager) { - this.entityManager = entityManager; - } - - @Transactional(readOnly = true) - @Override - public Optional findByFullPath(final String fullPath) { - if (StringUtils.isBlank(fullPath) - || (!SyncopeConstants.ROOT_REALM.equals(fullPath) - && !RealmDAO.PATH_PATTERN.matcher(fullPath).matches())) { - - throw new MalformedPathException(fullPath); - } - - TypedQuery query = entityManager.createQuery( - "SELECT e FROM " + JPARealm.class.getSimpleName() + " e WHERE e.fullPath=:fullPath", Realm.class); - query.setParameter("fullPath", fullPath); - - Realm result = null; - try { - result = query.getSingleResult(); - } catch (NoResultException e) { - LOG.debug("Realm with fullPath {} not found", fullPath, e); - } - - return Optional.ofNullable(result); - } - - @Override - public List findByName(final String name) { - TypedQuery query = entityManager.createQuery( - "SELECT e FROM " + JPARealm.class.getSimpleName() + " e WHERE e.name=:name", Realm.class); - query.setParameter("name", name); - - return query.getResultList(); - } - - @Override - public List findChildren(final Realm realm) { - TypedQuery query = entityManager.createQuery( - "SELECT e FROM " + JPARealm.class.getSimpleName() + " e WHERE e.parent=:realm", Realm.class); - query.setParameter("realm", realm); - - return query.getResultList(); - } - - @Override - public long countDescendants(final String base, final String keyword) { - return countDescendants(Set.of(base), keyword); - } - - @Override - public long countDescendants(final Set bases, final String keyword) { - List parameters = new ArrayList<>(); - - StringBuilder queryString = buildDescendantsQuery(bases, keyword, parameters); - Query query = entityManager.createQuery(Strings.CS.replaceOnce( - queryString.toString(), - "SELECT e ", - "SELECT COUNT(e) ")); - - for (int i = 1; i <= parameters.size(); i++) { - query.setParameter(i, parameters.get(i - 1)); - } - - return ((Number) query.getSingleResult()).longValue(); - } - - @Override - public List findDescendants(final String base, final String keyword, final Pageable pageable) { - return findDescendants(Set.of(base), keyword, pageable); - } - - @Override - public List findDescendants(final Set bases, final String keyword, final Pageable pageable) { - List parameters = new ArrayList<>(); - - StringBuilder queryString = buildDescendantsQuery(bases, keyword, parameters); - TypedQuery query = entityManager.createQuery( - queryString.append(" ORDER BY e.fullPath").toString(), Realm.class); - - for (int i = 1; i <= parameters.size(); i++) { - query.setParameter(i, parameters.get(i - 1)); - } - - if (pageable.isPaged()) { - query.setFirstResult(pageable.getPageSize() * pageable.getPageNumber()); - query.setMaxResults(pageable.getPageSize()); - } - - return query.getResultList(); - } - - @Override - public List findDescendants(final String base, final String prefix) { - List parameters = new ArrayList<>(); - - StringBuilder queryString = buildDescendantsQuery(Set.of(base), null, parameters); - TypedQuery query = entityManager.createQuery(queryString. - append(" AND (e.fullPath=?"). - append(setParameter(parameters, prefix)). - append(" OR e.fullPath LIKE ?"). - append(setParameter(parameters, SyncopeConstants.ROOT_REALM.equals(prefix) ? "/%" : prefix + "/%")). - append(')'). - append(" ORDER BY e.fullPath").toString(), - Realm.class); - - for (int i = 1; i <= parameters.size(); i++) { - query.setParameter(i, parameters.get(i - 1)); - } - - return query.getResultList().stream().map(Realm::getKey).toList(); - } -} diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPATaskDAO.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPATaskDAO.java index b5d656666aa..8007681f5a2 100644 --- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPATaskDAO.java +++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPATaskDAO.java @@ -337,7 +337,7 @@ protected StringBuilder buildFindAllQuery( String realmKeysArg = AuthContextUtils.getAuthorizations().get(IdRepoEntitlement.TASK_LIST).stream(). map(realmSearchDAO::findByFullPath). filter(Optional::isPresent). - flatMap(r -> realmSearchDAO.findDescendants(r.get().getFullPath(), null, Pageable.unpaged()). + flatMap(r -> realmSearchDAO.findDescendants(r.get().getFullPath(), null). stream()). map(Realm::getKey). distinct(). diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/MariaDBJPAAnySearchDAO.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/MariaDBJPAAnySearchDAO.java index 7e627b7e3f9..daa651c434c 100644 --- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/MariaDBJPAAnySearchDAO.java +++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/MariaDBJPAAnySearchDAO.java @@ -19,7 +19,6 @@ package org.apache.syncope.core.persistence.jpa.dao; import jakarta.persistence.EntityManager; -import jakarta.persistence.EntityManagerFactory; import org.apache.syncope.core.persistence.api.attrvalue.PlainAttrValidationManager; import org.apache.syncope.core.persistence.api.dao.AnyObjectDAO; import org.apache.syncope.core.persistence.api.dao.DynRealmDAO; @@ -42,7 +41,6 @@ public MariaDBJPAAnySearchDAO( final EntityFactory entityFactory, final AnyUtilsFactory anyUtilsFactory, final PlainAttrValidationManager validator, - final EntityManagerFactory entityManagerFactory, final EntityManager entityManager) { super( @@ -55,7 +53,6 @@ public MariaDBJPAAnySearchDAO( entityFactory, anyUtilsFactory, validator, - entityManagerFactory, entityManager); } } diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/MariaDBJPARealmSearchDAO.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/MariaDBJPARealmSearchDAO.java new file mode 100644 index 00000000000..ddd5af0b47d --- /dev/null +++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/MariaDBJPARealmSearchDAO.java @@ -0,0 +1,38 @@ +/* + * 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 jakarta.persistence.EntityManager; +import org.apache.syncope.core.persistence.api.attrvalue.PlainAttrValidationManager; +import org.apache.syncope.core.persistence.api.dao.PlainSchemaDAO; +import org.apache.syncope.core.persistence.api.entity.EntityFactory; +import org.apache.syncope.core.persistence.api.utils.RealmUtils; + +public class MariaDBJPARealmSearchDAO extends MySQLJPARealmSearchDAO { + + public MariaDBJPARealmSearchDAO( + final EntityManager entityManager, + final PlainSchemaDAO plainSchemaDAO, + final EntityFactory entityFactory, + final PlainAttrValidationManager validator, + final RealmUtils realmUtils) { + + super(entityManager, plainSchemaDAO, entityFactory, validator, realmUtils); + } +} diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/MySQLJPAAnySearchDAO.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/MySQLJPAAnySearchDAO.java index 31f8cebd698..d3508bcfc96 100644 --- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/MySQLJPAAnySearchDAO.java +++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/MySQLJPAAnySearchDAO.java @@ -19,7 +19,6 @@ package org.apache.syncope.core.persistence.jpa.dao; import jakarta.persistence.EntityManager; -import jakarta.persistence.EntityManagerFactory; import java.time.format.DateTimeFormatter; import java.util.List; import java.util.Optional; @@ -52,7 +51,6 @@ public MySQLJPAAnySearchDAO( final EntityFactory entityFactory, final AnyUtilsFactory anyUtilsFactory, final PlainAttrValidationManager validator, - final EntityManagerFactory entityManagerFactory, final EntityManager entityManager) { super( @@ -65,7 +63,6 @@ public MySQLJPAAnySearchDAO( entityFactory, anyUtilsFactory, validator, - entityManagerFactory, entityManager); } diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/MySQLJPARealmSearchDAO.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/MySQLJPARealmSearchDAO.java new file mode 100644 index 00000000000..2e530a72b7e --- /dev/null +++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/MySQLJPARealmSearchDAO.java @@ -0,0 +1,277 @@ +/* + * 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 jakarta.persistence.EntityManager; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import org.apache.commons.lang3.StringUtils; +import org.apache.syncope.common.lib.types.AttrSchemaType; +import org.apache.syncope.core.persistence.api.attrvalue.PlainAttrValidationManager; +import org.apache.syncope.core.persistence.api.dao.PlainSchemaDAO; +import org.apache.syncope.core.persistence.api.dao.search.AttrCond; +import org.apache.syncope.core.persistence.api.entity.EntityFactory; +import org.apache.syncope.core.persistence.api.entity.PlainAttr; +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.utils.RealmUtils; +import org.apache.syncope.core.persistence.jpa.entity.JPARealm; +import org.apache.syncope.core.provisioning.api.serialization.POJOHelper; +import org.springframework.data.domain.Sort; + +public class MySQLJPARealmSearchDAO extends AbstractJPARealmSearchDAO { + + protected static String from(final PlainSchema schema) { + String sqlType = switch (schema.getType()) { + case Long -> + "BIGINT"; + + case Double -> + "DOUBLE"; + + case Boolean -> + "VARCHAR(8)"; + + default -> + "VARCHAR(255)"; + }; + + if (schema.isUniqueConstraint()) { + return "JSON_TABLE(r.plainAttrs, '$[*]' COLUMNS (" + + "schemaz VARCHAR(255) PATH '$.schema', " + + "uniqueValue " + sqlType + " PATH '$.uniqueValue." + key(schema.getType()) + "'" + + ")) AS " + schema.getKey(); + } + + return "JSON_TABLE(r.plainAttrs, '$[*]' COLUMNS (" + + "schemaz VARCHAR(255) PATH '$.schema', " + + "NESTED PATH '$.values[*]' COLUMNS (" + + "valuez " + sqlType + " PATH '$." + key(schema.getType()) + "'" + + "))) AS " + schema.getKey(); + } + + public MySQLJPARealmSearchDAO( + final EntityManager entityManager, + final PlainSchemaDAO plainSchemaDAO, + final EntityFactory entityFactory, + final PlainAttrValidationManager validator, + final RealmUtils realmUtils) { + + super(entityManager, plainSchemaDAO, entityFactory, validator, realmUtils); + } + + @Override + protected void parseOrderByForPlainSchema( + final OrderBySupport obs, + final OrderBySupport.Item item, + final Sort.Order clause, + final PlainSchema schema, + final String fieldName) { + + // keep track of involvement of non-mandatory schemas in the order by clauses + obs.nonMandatorySchemas = !"true".equals(schema.getMandatoryCondition()); + + item.select = schema.getKey() + "." + + (schema.isUniqueConstraint() ? "uniqueValue" : "valuez") + + " AS " + schema.getKey(); + item.where = StringUtils.EMPTY; + item.orderBy = fieldName + ' ' + clause.getDirection().name(); + } + + protected RealmSearchNode.Leaf filJSONAttrQuery( + final PlainAttrValue attrValue, + final PlainSchema schema, + final AttrCond cond, + final boolean not, + final List parameters) { + + String value = Optional.ofNullable(attrValue.getDateValue()). + map(DateTimeFormatter.ISO_OFFSET_DATE_TIME::format). + orElseGet(cond::getExpression); + Object typedValue = schema.getType() == AttrSchemaType.Date ? value : attrValue.getValue(); + + boolean isString = schema.getType() == AttrSchemaType.String || schema.getType() == AttrSchemaType.Enum; + boolean lower = isString && (cond.getType() == AttrCond.Type.IEQ || cond.getType() == AttrCond.Type.ILIKE); + boolean binary = isString && !lower; + + StringBuilder clause = new StringBuilder(schema.getKey()). + append(".schemaz=?").append(setParameter(parameters, cond.getSchema())). + append(" AND "). + append(lower ? "LOWER(" : ""). + append(binary ? "BINARY " : ""). + append(schema.getKey()).append('.'). + append(schema.isUniqueConstraint() + ? "uniqueValue" + : "valuez"). + append(lower ? ')' : ""); + + switch (cond.getType()) { + case LIKE: + case ILIKE: + if (not) { + clause.append(" NOT"); + } + clause.append(" LIKE "); + break; + + case GE: + if (not) { + clause.append('<'); + } else { + clause.append(">="); + } + break; + + case GT: + if (not) { + clause.append("<="); + } else { + clause.append('>'); + } + break; + + case LE: + if (not) { + clause.append('>'); + } else { + clause.append("<="); + } + break; + + case LT: + if (not) { + clause.append(">="); + } else { + clause.append('<'); + } + break; + + case EQ: + case IEQ: + default: + if (not) { + clause.append('!'); + } + clause.append('='); + } + + clause.append(lower ? "LOWER(" : ""). + append('?').append(setParameter(parameters, + cond.getType() == AttrCond.Type.LIKE || cond.getType() == AttrCond.Type.ILIKE + ? value + : typedValue)). + append(lower ? ")" : ""); + + return new RealmSearchNode.Leaf(clause.toString()); + } + + @Override + protected AttrCondQuery getQuery( + final AttrCond cond, + final boolean not, + final CheckResult checked, + final List parameters) { + + if (not) { + if (cond.getType() == AttrCond.Type.ISNULL) { + cond.setType(AttrCond.Type.ISNOTNULL); + } else if (cond.getType() == AttrCond.Type.ISNOTNULL) { + cond.setType(AttrCond.Type.ISNULL); + } + } + + switch (cond.getType()) { + case ISNOTNULL -> { + return new AttrCondQuery(true, new RealmSearchNode.Leaf( + "JSON_SEARCH(" + + "r.plainAttrs, 'one', '" + checked.schema().getKey() + "', NULL, '$[*].schema'" + + ") IS NOT NULL")); + } + + case ISNULL -> { + return new AttrCondQuery(true, new RealmSearchNode.Leaf( + "JSON_SEARCH(" + + "r.plainAttrs, 'one', '" + checked.schema().getKey() + "', NULL, '$[*].schema'" + + ") IS NULL")); + } + + default -> { + if (!not && cond.getType() == AttrCond.Type.EQ) { + PlainAttr container = new PlainAttr(); + container.setPlainSchema(checked.schema()); + if (checked.schema().isUniqueConstraint()) { + container.setUniqueValue(checked.value()); + } else { + container.add(checked.value()); + } + + return new AttrCondQuery(true, new RealmSearchNode.Leaf( + "JSON_CONTAINS(" + + "r.plainAttrs, '" + POJOHelper.serialize(List.of(container)).replace("'", "''") + + "')")); + } else { + RealmSearchNode.Leaf node; + if (not && checked.schema().isMultivalue()) { + RealmSearchNode.Leaf notNode = filJSONAttrQuery( + checked.value(), + checked.schema(), + cond, + false, + parameters); + node = new RealmSearchNode.Leaf( + "r.id NOT IN (" + + "SELECT r.id FROM " + JPARealm.TABLE + " r, " + from(checked.schema()) + + " WHERE " + notNode.getClause() + + ")"); + } else { + node = filJSONAttrQuery( + checked.value(), + checked.schema(), + cond, + not, + parameters); + } + return new AttrCondQuery(true, node); + } + } + } + } + + @Override + protected String buildFrom(final Set plainSchemas, final OrderBySupport obs) { + StringBuilder clause = new StringBuilder(super.buildFrom(plainSchemas, obs)); + + plainSchemas.forEach(schema -> plainSchemaDAO.findById(schema). + ifPresent(pschema -> clause.append(",").append(from(pschema)))); + + if (obs != null) { + obs.items.forEach(item -> { + String schema = StringUtils.substringBefore(item.orderBy, ' '); + if (StringUtils.isNotBlank(schema) && !plainSchemas.contains(schema)) { + plainSchemaDAO.findById(schema).ifPresent( + pschema -> clause.append(" LEFT OUTER JOIN ").append(from(pschema)).append(" ON 1=1")); + } + }); + } + + return clause.toString(); + } +} diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/OracleJPAAnySearchDAO.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/OracleJPAAnySearchDAO.java index ad63eb5260d..05dd9bfbb71 100644 --- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/OracleJPAAnySearchDAO.java +++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/OracleJPAAnySearchDAO.java @@ -19,7 +19,6 @@ package org.apache.syncope.core.persistence.jpa.dao; import jakarta.persistence.EntityManager; -import jakarta.persistence.EntityManagerFactory; import java.time.format.DateTimeFormatter; import java.util.List; import java.util.Map; @@ -69,7 +68,6 @@ public OracleJPAAnySearchDAO( final EntityFactory entityFactory, final AnyUtilsFactory anyUtilsFactory, final PlainAttrValidationManager validator, - final EntityManagerFactory entityManagerFactory, final EntityManager entityManager) { super( @@ -82,7 +80,6 @@ public OracleJPAAnySearchDAO( entityFactory, anyUtilsFactory, validator, - entityManagerFactory, entityManager); } diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/OracleJPARealmSearchDAO.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/OracleJPARealmSearchDAO.java new file mode 100644 index 00000000000..2b45aab33d1 --- /dev/null +++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/OracleJPARealmSearchDAO.java @@ -0,0 +1,207 @@ +/* + * 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 jakarta.persistence.EntityManager; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import org.apache.commons.lang3.StringUtils; +import org.apache.syncope.common.lib.types.AttrSchemaType; +import org.apache.syncope.core.persistence.api.attrvalue.PlainAttrValidationManager; +import org.apache.syncope.core.persistence.api.dao.PlainSchemaDAO; +import org.apache.syncope.core.persistence.api.dao.search.AttrCond; +import org.apache.syncope.core.persistence.api.entity.EntityFactory; +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.utils.RealmUtils; +import org.apache.syncope.core.persistence.jpa.entity.JPARealm; +import org.springframework.data.domain.Sort; + +public class OracleJPARealmSearchDAO extends AbstractJPARealmSearchDAO { + + protected static String from(final PlainSchema schema) { + return new StringBuilder("JSON_TABLE(plainAttrs, '$[*]?(@.schema == \"").append(schema.getKey()).append("\")."). + append(schema.isUniqueConstraint() ? "uniqueValue" : "values[*]"). + append("' COLUMNS ").append(schema.isUniqueConstraint() ? "uniqueValue" : "valuez"). + append(" PATH '$.").append(key(schema.getType())).append("') AS ").append(schema.getKey()). + toString(); + } + + public OracleJPARealmSearchDAO( + final EntityManager entityManager, + final PlainSchemaDAO plainSchemaDAO, + final EntityFactory entityFactory, + final PlainAttrValidationManager validator, + final RealmUtils realmUtils) { + + super(entityManager, plainSchemaDAO, entityFactory, validator, realmUtils); + } + + @Override + protected void parseOrderByForPlainSchema( + final OrderBySupport obs, + final OrderBySupport.Item item, + final Sort.Order clause, + final PlainSchema schema, + final String fieldName) { + + // keep track of involvement of non-mandatory schemas in the order by clauses + obs.nonMandatorySchemas = !"true".equals(schema.getMandatoryCondition()); + + item.select = schema.getKey() + "." + + (schema.isUniqueConstraint() ? "uniqueValue" : "valuez") + + " AS " + schema.getKey(); + item.where = StringUtils.EMPTY; + item.orderBy = fieldName + ' ' + clause.getDirection().name(); + } + + protected RealmSearchNode.Leaf filJSONAttrQuery( + final PlainAttrValue attrValue, + final PlainSchema schema, + final AttrCond cond, + final boolean not, + final List parameters) { + + String value = Optional.ofNullable(attrValue.getDateValue()). + map(DateTimeFormatter.ISO_OFFSET_DATE_TIME::format). + orElseGet(cond::getExpression); + Object typedValue = schema.getType() == AttrSchemaType.Date ? value : attrValue.getValue(); + + boolean lower = (schema.getType() == AttrSchemaType.String || schema.getType() == AttrSchemaType.Enum) + && (cond.getType() == AttrCond.Type.IEQ || cond.getType() == AttrCond.Type.ILIKE); + + StringBuilder clause = new StringBuilder(lower ? "LOWER(" : ""). + append(schema.getKey()).append('.').append(schema.isUniqueConstraint() ? "uniqueValue" : "valuez"). + append(lower ? ')' : ""); + + switch (cond.getType()) { + case LIKE: + case ILIKE: + if (not) { + clause.append(" NOT"); + } + clause.append(" LIKE "). + append(lower ? "LOWER(" : ""). + append('?').append(setParameter(parameters, value)). + append(lower ? ')' : "").append(" ESCAPE '\\'"); + break; + + case GE: + clause.append(not ? "<" : ">="). + append('?').append(setParameter(parameters, typedValue)); + break; + + case GT: + clause.append(not ? "<=" : ">"). + append('?').append(setParameter(parameters, typedValue)); + break; + + case LE: + clause.append(not ? ">" : "<="). + append('?').append(setParameter(parameters, typedValue)); + break; + + case LT: + clause.append(not ? ">=" : "<"). + append('?').append(setParameter(parameters, typedValue)); + break; + + case IEQ: + case EQ: + default: + clause.append(not ? " != " : " = "). + append(lower ? "LOWER(" : ""). + append('?').append(setParameter(parameters, typedValue)). + append(lower ? ")" : ""); + break; + } + + return new RealmSearchNode.Leaf(clause.toString()); + } + + @Override + protected AttrCondQuery getQuery( + final AttrCond cond, + final boolean not, + final CheckResult checked, + final List parameters) { + + if (not) { + if (cond.getType() == AttrCond.Type.ISNULL) { + cond.setType(AttrCond.Type.ISNOTNULL); + } else if (cond.getType() == AttrCond.Type.ISNOTNULL) { + cond.setType(AttrCond.Type.ISNULL); + } + } + + switch (cond.getType()) { + case ISNOTNULL -> { + return new AttrCondQuery(false, new RealmSearchNode.Leaf( + "JSON_EXISTS(r.plainAttrs, '$[*]?(@.schema == \"" + + checked.schema().getKey() + "\")')")); + } + + case ISNULL -> { + return new AttrCondQuery(false, new RealmSearchNode.Leaf( + "NOT JSON_EXISTS(r.plainAttrs, '$[*]?(@.schema == \"" + + checked.schema().getKey() + "\")')")); + } + + default -> { + RealmSearchNode.Leaf node; + if (not && checked.schema().isMultivalue()) { + RealmSearchNode.Leaf notNode = filJSONAttrQuery( + checked.value(), checked.schema(), cond, false, parameters); + node = new RealmSearchNode.Leaf( + "id NOT IN (" + + "SELECT id FROM " + JPARealm.TABLE + " e, " + from(checked.schema()) + + " WHERE " + notNode.getClause() + + ")"); + return new AttrCondQuery(false, node); + } else { + node = filJSONAttrQuery(checked.value(), checked.schema(), cond, not, parameters); + } + return new AttrCondQuery(true, node); + } + + } + } + + @Override + protected String buildFrom(final Set plainSchemas, final OrderBySupport obs) { + StringBuilder clause = new StringBuilder(super.buildFrom(plainSchemas, obs)); + + plainSchemas.forEach(schema -> plainSchemaDAO.findById(schema). + ifPresent(pschema -> clause.append(",").append(from(pschema)))); + + if (obs != null) { + obs.items.forEach(item -> { + String schema = StringUtils.substringBefore(item.orderBy, ' '); + if (StringUtils.isNotBlank(schema) && !plainSchemas.contains(schema)) { + plainSchemaDAO.findById(schema).ifPresent( + pschema -> clause.append(" LEFT OUTER JOIN ").append(from(pschema)).append(" ON 1=1")); + } + }); + } + + return clause.toString(); + } +} diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/PGJPAAnySearchDAO.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/PGJPAAnySearchDAO.java index 28cd12b1ba7..ac152b8d30a 100644 --- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/PGJPAAnySearchDAO.java +++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/PGJPAAnySearchDAO.java @@ -19,7 +19,6 @@ package org.apache.syncope.core.persistence.jpa.dao; import jakarta.persistence.EntityManager; -import jakarta.persistence.EntityManagerFactory; import java.time.format.DateTimeFormatter; import java.util.HashSet; import java.util.List; @@ -70,7 +69,6 @@ public PGJPAAnySearchDAO( final EntityFactory entityFactory, final AnyUtilsFactory anyUtilsFactory, final PlainAttrValidationManager validator, - final EntityManagerFactory entityManagerFactory, final EntityManager entityManager) { super( @@ -83,7 +81,6 @@ public PGJPAAnySearchDAO( entityFactory, anyUtilsFactory, validator, - entityManagerFactory, entityManager); } diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/PGJPARealmSearchDAO.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/PGJPARealmSearchDAO.java new file mode 100644 index 00000000000..ad82011b179 --- /dev/null +++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/PGJPARealmSearchDAO.java @@ -0,0 +1,261 @@ +/* + * 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 jakarta.persistence.EntityManager; +import java.time.format.DateTimeFormatter; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import org.apache.commons.lang3.StringUtils; +import org.apache.syncope.common.lib.types.AttrSchemaType; +import org.apache.syncope.core.persistence.api.attrvalue.PlainAttrValidationManager; +import org.apache.syncope.core.persistence.api.dao.PlainSchemaDAO; +import org.apache.syncope.core.persistence.api.dao.search.AttrCond; +import org.apache.syncope.core.persistence.api.entity.EntityFactory; +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.utils.RealmUtils; +import org.springframework.data.domain.Sort; + +public class PGJPARealmSearchDAO extends AbstractJPARealmSearchDAO { + + protected static final String REGEX_CHARS = "!$()*+.:<=>?[\\]^{|}-"; + + protected static String escapeForLikeRegex(final String input) { + String output = input; + for (char toEscape : REGEX_CHARS.toCharArray()) { + output = output.replace(String.valueOf(toEscape), "\\" + toEscape); + } + return output.replace("'", "''"); + } + + protected static String escapeIfString(final String value, final boolean isStr) { + return isStr ? '"' + value.replace("'", "''") + '"' : value; + } + + public PGJPARealmSearchDAO( + final EntityManager entityManager, + final PlainSchemaDAO plainSchemaDAO, + final EntityFactory entityFactory, + final PlainAttrValidationManager validator, + final RealmUtils realmUtils) { + + super(entityManager, plainSchemaDAO, entityFactory, validator, realmUtils); + } + + @Override + protected void parseOrderByForPlainSchema( + final OrderBySupport obs, + final OrderBySupport.Item item, + final Sort.Order clause, + final PlainSchema schema, + final String fieldName) { + + // keep track of involvement of non-mandatory schemas in the order by clauses + obs.nonMandatorySchemas = !"true".equals(schema.getMandatoryCondition()); + + item.select = fieldName + " -> 0 AS " + fieldName; + item.where = StringUtils.EMPTY; + item.orderBy = fieldName + ' ' + clause.getDirection().name(); + } + + protected RealmSearchNode.Leaf filJSONAttrQuery( + final PlainAttrValue attrValue, + final PlainSchema schema, + final AttrCond cond, + final boolean not) { + + String key = key(schema.getType()); + String valuesPath = schema.isUniqueConstraint() ? "uniqueValue" : "values[*]"; + + String value = Optional.ofNullable(attrValue.getDateValue()). + map(DateTimeFormatter.ISO_OFFSET_DATE_TIME::format). + orElseGet(cond::getExpression); + + boolean isStr = true; + boolean lower = false; + if (schema.getType() == AttrSchemaType.String || schema.getType() == AttrSchemaType.Enum) { + lower = cond.getType() == AttrCond.Type.IEQ || cond.getType() == AttrCond.Type.ILIKE; + } else if (schema.getType() != AttrSchemaType.Date) { + try { + switch (schema.getType()) { + case Long -> + Long.valueOf(value); + case Double -> + Double.valueOf(value); + case Boolean -> { + if (!("true".equalsIgnoreCase(value) || "false".equalsIgnoreCase(value))) { + throw new IllegalArgumentException(); + } + } + default -> { + } + } + isStr = false; + } catch (Exception nfe) { + // ignore — treat as string + } + } + + StringBuilder clause = new StringBuilder(); + switch (cond.getType()) { + case ILIKE: + case LIKE: + if (schema.getType() == AttrSchemaType.String || schema.getType() == AttrSchemaType.Enum) { + clause.append("jsonb_path_exists(r.plainAttrs, '$[*] ? (@.schema == \""). + append(schema.getKey()).append("\").").append(valuesPath). + append(" ? (@.").append(key).append(" like_regex \""). + append(escapeForLikeRegex(value).replace("%", ".*")).append("\""). + append(lower ? " flag \"i\"" : "").append(")')"); + } else { + LOG.error("LIKE is only compatible with string or enum schemas"); + clause.append(ALWAYS_FALSE_CLAUSE); + } + break; + + case IEQ: + case EQ: + default: + clause.append("jsonb_path_exists(r.plainAttrs, '$[*] ? (@.schema == \""). + append(schema.getKey()).append("\").").append(valuesPath). + append(" ? ("); + if (StringUtils.containsAny(value, REGEX_CHARS) || lower) { + clause.append("@.").append(key).append(" like_regex \"^"). + append(escapeForLikeRegex(value).replace("'", "''")).append("$\""); + } else if (isStr) { + clause.append("@.").append(key).append(" == ").append(escapeIfString(value, true)); + } else { + // Some datasets can store scalar values as JSON strings; accept both representations. + clause.append("@.").append(key).append(" == ").append(escapeIfString(value, false)). + append(" || @.").append(key).append(" == ").append(escapeIfString(value, true)). + append(" || @.stringValue == ").append(escapeIfString(value, true)); + } + clause.append(lower ? " flag \"i\"" : "").append(")')"); + break; + + case GE: + clause.append("jsonb_path_exists(r.plainAttrs, '$[*] ? (@.schema == \""). + append(schema.getKey()).append("\").").append(valuesPath). + append(" ? (@.").append(key).append(" >= "). + append(escapeIfString(value, isStr)).append(")')"); + break; + + case GT: + clause.append("jsonb_path_exists(r.plainAttrs, '$[*] ? (@.schema == \""). + append(schema.getKey()).append("\").").append(valuesPath). + append(" ? (@.").append(key).append(" > "). + append(escapeIfString(value, isStr)).append(")')"); + break; + + case LE: + clause.append("jsonb_path_exists(r.plainAttrs, '$[*] ? (@.schema == \""). + append(schema.getKey()).append("\").").append(valuesPath). + append(" ? (@.").append(key).append(" <= "). + append(escapeIfString(value, isStr)).append(")')"); + break; + + case LT: + clause.append("jsonb_path_exists(r.plainAttrs, '$[*] ? (@.schema == \""). + append(schema.getKey()).append("\").").append(valuesPath). + append(" ? (@.").append(key).append(" < "). + append(escapeIfString(value, isStr)).append(")')"); + break; + } + + if (not) { + clause.insert(0, "NOT "); + } + + return new RealmSearchNode.Leaf(clause.toString()); + } + + @Override + protected AttrCondQuery getQuery( + final AttrCond cond, + final boolean not, + final CheckResult checked, + final List parameters) { + + if (not) { + if (cond.getType() == AttrCond.Type.ISNULL) { + cond.setType(AttrCond.Type.ISNOTNULL); + } else if (cond.getType() == AttrCond.Type.ISNOTNULL) { + cond.setType(AttrCond.Type.ISNULL); + } + } + + switch (cond.getType()) { + case ISNOTNULL -> { + return new AttrCondQuery(true, new RealmSearchNode.Leaf( + "jsonb_path_exists(r.plainAttrs, '$[*] ? (@.schema == \"" + + checked.schema().getKey() + "\")')")); + } + + case ISNULL -> { + return new AttrCondQuery(true, new RealmSearchNode.Leaf( + "NOT jsonb_path_exists(r.plainAttrs, '$[*] ? (@.schema == \"" + + checked.schema().getKey() + "\")')")); + } + + default -> { + RealmSearchNode.Leaf node; + if (not && checked.schema().isMultivalue()) { + // negate multivalue by wrapping the positive expression in NOT + RealmSearchNode.Leaf notNode = filJSONAttrQuery( + checked.value(), checked.schema(), cond, false); + node = new RealmSearchNode.Leaf("NOT " + notNode.getClause()); + } else { + node = filJSONAttrQuery(checked.value(), checked.schema(), cond, not); + } + return new AttrCondQuery(true, node); + } + } + } + + @Override + protected String buildFrom( + final Set plainSchemas, + final OrderBySupport obs) { + + StringBuilder clause = new StringBuilder(super.buildFrom(plainSchemas, obs)); + + Set schemas = new HashSet<>(plainSchemas); + + if (obs != null) { + obs.items.forEach(item -> { + String schema = StringUtils.substringBefore(item.orderBy, ' '); + if (StringUtils.isNotBlank(schema)) { + schemas.add(schema); + } + }); + } + + // i.e jsonb_path_query(plainattrs, '$[*] ? (@.schema=="Nome")."values"') AS Nome + schemas.forEach(schema -> plainSchemaDAO.findById(schema).ifPresent( + pschema -> clause.append(','). + append("jsonb_path_query_array(plainattrs, '$[*] ? (@.schema==\""). + append(schema).append("\")."). + append("\"").append(pschema.isUniqueConstraint() ? "uniqueValue" : "values").append("\"')"). + append(" AS ").append(schema))); + + return clause.toString(); + } +} diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/RealmSearchNode.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/RealmSearchNode.java new file mode 100644 index 00000000000..b9b5c08b6b2 --- /dev/null +++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/RealmSearchNode.java @@ -0,0 +1,141 @@ +/* + * 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.List; +import java.util.Optional; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; + +public class RealmSearchNode { + + public enum Type { + AND, + OR, + LEAF + } + + public static class Leaf extends RealmSearchNode { + + private final String clause; + + protected Leaf(final String clause) { + super(Type.LEAF); + this.clause = clause; + } + + public String getClause() { + return clause; + } + + @Override + public int hashCode() { + return new HashCodeBuilder(). + appendSuper(super.hashCode()). + append(clause). + build(); + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final Leaf other = (Leaf) obj; + + return new EqualsBuilder(). + appendSuper(super.equals(obj)). + append(clause, other.clause). + build(); + } + + @Override + public String toString() { + return "LeafNode{clause=" + clause + '}'; + } + } + + private final Type type; + + private final List children = new ArrayList<>(); + + public RealmSearchNode(final Type type) { + this.type = type; + } + + protected Type getType() { + return type; + } + + protected boolean add(final RealmSearchNode child) { + if (type == Type.LEAF) { + throw new IllegalArgumentException("Cannot add children to a leaf node"); + } + return children.add(child); + } + + protected List getChildren() { + return children; + } + + protected Optional asLeaf() { + return type == Type.LEAF + ? Optional.of((Leaf) this) + : Optional.empty(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder(). + append(type). + append(children). + build(); + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final RealmSearchNode other = (RealmSearchNode) obj; + + return new EqualsBuilder(). + append(type, other.type). + append(children, other.children). + build(); + } + + @Override + public String toString() { + return "Node{" + "type=" + type + ", children=" + children + '}'; + } +} diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/repo/AbstractAnyRepoExt.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/repo/AbstractAnyRepoExt.java index bd2e29ff3ae..57d1a1a66df 100644 --- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/repo/AbstractAnyRepoExt.java +++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/repo/AbstractAnyRepoExt.java @@ -46,7 +46,6 @@ 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.user.User; -import org.apache.syncope.core.persistence.common.dao.AnyFinder; import org.apache.syncope.core.persistence.jpa.entity.AbstractAttributable; import org.apache.syncope.core.persistence.jpa.entity.anyobject.JPAAnyObject; import org.apache.syncope.core.persistence.jpa.entity.group.JPAGroup; @@ -68,8 +67,6 @@ public abstract class AbstractAnyRepoExt implements AnyRepoExt protected final EntityManager entityManager; - protected final AnyFinder anyFinder; - protected final AnyUtils anyUtils; protected final String table; @@ -78,13 +75,11 @@ protected AbstractAnyRepoExt( final DynRealmDAO dynRealmDAO, final PlainSchemaDAO plainSchemaDAO, final EntityManager entityManager, - final AnyFinder anyFinder, final AnyUtils anyUtils) { this.dynRealmDAO = dynRealmDAO; this.plainSchemaDAO = plainSchemaDAO; this.entityManager = entityManager; - this.anyFinder = anyFinder; this.anyUtils = anyUtils; switch (anyUtils.anyTypeKind()) { case ANY_OBJECT: @@ -141,11 +136,6 @@ public A authFind(final String key) { return any; } - @Override - public List findByDerAttrValue(final String expression, final String value, final boolean ignoreCaseMatch) { - return anyFinder.findByDerAttrValue(anyUtils.anyTypeKind(), expression, value, ignoreCaseMatch); - } - @Transactional(propagation = Propagation.REQUIRES_NEW, readOnly = true) @Override @SuppressWarnings("unchecked") diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/repo/AnyObjectRepoExtImpl.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/repo/AnyObjectRepoExtImpl.java index ae9ffad0eb3..21d0dc009ef 100644 --- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/repo/AnyObjectRepoExtImpl.java +++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/repo/AnyObjectRepoExtImpl.java @@ -50,7 +50,6 @@ import org.apache.syncope.core.persistence.api.entity.group.Group; import org.apache.syncope.core.persistence.api.entity.user.URelationship; import org.apache.syncope.core.persistence.api.utils.RealmUtils; -import org.apache.syncope.core.persistence.common.dao.AnyFinder; import org.apache.syncope.core.persistence.jpa.entity.anyobject.JPAAMembership; import org.apache.syncope.core.persistence.jpa.entity.anyobject.JPAARelationship; import org.apache.syncope.core.persistence.jpa.entity.anyobject.JPAAnyObject; @@ -72,14 +71,12 @@ public AnyObjectRepoExtImpl( final PlainSchemaDAO plainSchemaDAO, final UserDAO userDAO, final GroupDAO groupDAO, - final EntityManager entityManager, - final AnyFinder anyFinder) { + final EntityManager entityManager) { super( dynRealmDAO, plainSchemaDAO, entityManager, - anyFinder, anyUtilsFactory.getInstance(AnyTypeKind.ANY_OBJECT)); this.userDAO = userDAO; this.groupDAO = groupDAO; diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/repo/AnyRepoExt.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/repo/AnyRepoExt.java index 3e28c17b3d3..18cf016725f 100644 --- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/repo/AnyRepoExt.java +++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/repo/AnyRepoExt.java @@ -32,8 +32,6 @@ public interface AnyRepoExt { A authFind(String key); - List findByDerAttrValue(String expression, String value, boolean ignoreCaseMatch); - AllowedSchemas findAllowedSchemas(A any, Class reference); List findDynRealms(String key); diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/repo/DynRealmRepoExtImpl.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/repo/DynRealmRepoExtImpl.java index 9f8f4762812..39119f3671a 100644 --- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/repo/DynRealmRepoExtImpl.java +++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/repo/DynRealmRepoExtImpl.java @@ -30,8 +30,8 @@ import org.apache.syncope.core.persistence.api.dao.UserDAO; import org.apache.syncope.core.persistence.api.entity.Any; import org.apache.syncope.core.persistence.api.entity.DynRealm; +import org.apache.syncope.core.persistence.api.search.AnySearchCondVisitor; import org.apache.syncope.core.persistence.api.search.SearchCondConverter; -import org.apache.syncope.core.persistence.api.search.SearchCondVisitor; import org.apache.syncope.core.persistence.jpa.entity.JPADynRealm; import org.apache.syncope.core.provisioning.api.event.EntityLifecycleEvent; import org.apache.syncope.core.spring.security.AuthContextUtils; @@ -53,7 +53,7 @@ public class DynRealmRepoExtImpl implements DynRealmRepoExt { protected final AnyMatchDAO anyMatchDAO; - protected final SearchCondVisitor searchCondVisitor; + protected final AnySearchCondVisitor searchCondVisitor; protected final EntityManager entityManager; @@ -64,7 +64,7 @@ public DynRealmRepoExtImpl( final AnyObjectDAO anyObjectDAO, final AnySearchDAO searchDAO, final AnyMatchDAO anyMatchDAO, - final SearchCondVisitor searchCondVisitor, + final AnySearchCondVisitor searchCondVisitor, final EntityManager entityManager) { this.publisher = publisher; diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/repo/GroupRepoExtImpl.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/repo/GroupRepoExtImpl.java index bb561b151f8..ef5eb91990f 100644 --- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/repo/GroupRepoExtImpl.java +++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/repo/GroupRepoExtImpl.java @@ -55,10 +55,9 @@ import org.apache.syncope.core.persistence.api.entity.user.UDynGroupMembership; import org.apache.syncope.core.persistence.api.entity.user.UMembership; import org.apache.syncope.core.persistence.api.entity.user.User; +import org.apache.syncope.core.persistence.api.search.AnySearchCondVisitor; import org.apache.syncope.core.persistence.api.search.SearchCondConverter; -import org.apache.syncope.core.persistence.api.search.SearchCondVisitor; import org.apache.syncope.core.persistence.api.utils.RealmUtils; -import org.apache.syncope.core.persistence.common.dao.AnyFinder; import org.apache.syncope.core.persistence.jpa.entity.anyobject.JPAADynGroupMembership; import org.apache.syncope.core.persistence.jpa.entity.anyobject.JPAAMembership; import org.apache.syncope.core.persistence.jpa.entity.group.JPAGroup; @@ -88,7 +87,7 @@ public class GroupRepoExtImpl extends AbstractAnyRepoExt implements Group protected final AnySearchDAO anySearchDAO; - protected final SearchCondVisitor searchCondVisitor; + protected final AnySearchCondVisitor searchCondVisitor; public GroupRepoExtImpl( final AnyUtilsFactory anyUtilsFactory, @@ -100,15 +99,13 @@ public GroupRepoExtImpl( final UserDAO userDAO, final AnyObjectDAO anyObjectDAO, final AnySearchDAO searchDAO, - final SearchCondVisitor searchCondVisitor, - final EntityManager entityManager, - final AnyFinder anyFinder) { + final AnySearchCondVisitor searchCondVisitor, + final EntityManager entityManager) { super( dynRealmDAO, plainSchemaDAO, entityManager, - anyFinder, anyUtilsFactory.getInstance(AnyTypeKind.GROUP)); this.publisher = publisher; this.realmDAO = realmDAO; diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/repo/RoleRepoExtImpl.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/repo/RoleRepoExtImpl.java index a3e94a69b43..2e906557c99 100644 --- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/repo/RoleRepoExtImpl.java +++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/repo/RoleRepoExtImpl.java @@ -28,8 +28,8 @@ import org.apache.syncope.core.persistence.api.dao.DelegationDAO; import org.apache.syncope.core.persistence.api.entity.Role; import org.apache.syncope.core.persistence.api.entity.user.User; +import org.apache.syncope.core.persistence.api.search.AnySearchCondVisitor; import org.apache.syncope.core.persistence.api.search.SearchCondConverter; -import org.apache.syncope.core.persistence.api.search.SearchCondVisitor; import org.apache.syncope.core.persistence.jpa.entity.JPARole; import org.apache.syncope.core.persistence.jpa.entity.user.JPAUser; import org.apache.syncope.core.provisioning.api.event.EntityLifecycleEvent; @@ -48,7 +48,7 @@ public class RoleRepoExtImpl implements RoleRepoExt { protected final DelegationDAO delegationDAO; - protected final SearchCondVisitor searchCondVisitor; + protected final AnySearchCondVisitor searchCondVisitor; protected final EntityManager entityManager; @@ -57,7 +57,7 @@ public RoleRepoExtImpl( final AnyMatchDAO anyMatchDAO, final AnySearchDAO anySearchDAO, final DelegationDAO delegationDAO, - final SearchCondVisitor searchCondVisitor, + final AnySearchCondVisitor searchCondVisitor, final EntityManager entityManager) { this.publisher = publisher; diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/repo/UserRepoExtImpl.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/repo/UserRepoExtImpl.java index 4446956a778..6b5dd8f216e 100644 --- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/repo/UserRepoExtImpl.java +++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/repo/UserRepoExtImpl.java @@ -46,7 +46,6 @@ import org.apache.syncope.core.persistence.api.entity.user.UMembership; import org.apache.syncope.core.persistence.api.entity.user.User; import org.apache.syncope.core.persistence.api.utils.RealmUtils; -import org.apache.syncope.core.persistence.common.dao.AnyFinder; import org.apache.syncope.core.persistence.jpa.entity.user.JPALinkedAccount; import org.apache.syncope.core.persistence.jpa.entity.user.JPAUMembership; import org.apache.syncope.core.persistence.jpa.entity.user.JPAUser; @@ -80,14 +79,12 @@ public UserRepoExtImpl( final DelegationDAO delegationDAO, final FIQLQueryDAO fiqlQueryDAO, final SecurityProperties securityProperties, - final EntityManager entityManager, - final AnyFinder anyFinder) { + final EntityManager entityManager) { super( dynRealmDAO, plainSchemaDAO, entityManager, - anyFinder, anyUtilsFactory.getInstance(AnyTypeKind.USER)); this.roleDAO = roleDAO; this.accessTokenDAO = accessTokenDAO; diff --git a/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/AnySearchTest.java b/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/AnySearchTest.java index 1891f8c4e87..9d4f71e0950 100644 --- a/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/AnySearchTest.java +++ b/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/AnySearchTest.java @@ -41,6 +41,7 @@ import org.apache.syncope.core.persistence.api.dao.AnyObjectDAO; import org.apache.syncope.core.persistence.api.dao.AnySearchDAO; import org.apache.syncope.core.persistence.api.dao.AnyTypeDAO; +import org.apache.syncope.core.persistence.api.dao.DerSchemaDAO; import org.apache.syncope.core.persistence.api.dao.GroupDAO; import org.apache.syncope.core.persistence.api.dao.PlainSchemaDAO; import org.apache.syncope.core.persistence.api.dao.RealmDAO; @@ -113,6 +114,9 @@ public class AnySearchTest extends AbstractTest { @Autowired private PlainSchemaDAO plainSchemaDAO; + @Autowired + private DerSchemaDAO derSchemaDAO; + @Autowired private PlainAttrValidationManager validator; @@ -723,13 +727,13 @@ public void asGroupOwner() { assertEquals( 1, searchDAO.count( - realmDAO.getRoot(), true, authRealms, groupDAO.getAllMatchingCond(), AnyTypeKind.GROUP)); + realmDAO.getRoot(), true, authRealms, searchDAO.getAllMatchingCond(), AnyTypeKind.GROUP)); List groups = searchDAO.search( realmDAO.getRoot(), true, authRealms, - groupDAO.getAllMatchingCond(), + searchDAO.getAllMatchingCond(), PageRequest.of(0, 10), AnyTypeKind.GROUP); assertEquals(1, groups.size()); @@ -756,6 +760,35 @@ public void changePwdDate() { assertEquals(5, users.size()); } + @Test + public void findByDerAttrValue() { + List list = searchDAO.findByDerAttrValue( + derSchemaDAO.findById("cn").orElseThrow().getExpression(), "Vivaldi, Antonio", false, AnyTypeKind.USER); + assertEquals(1, list.size()); + + list = searchDAO.findByDerAttrValue( + derSchemaDAO.findById("cn").orElseThrow().getExpression(), "VIVALDI, ANTONIO", false, AnyTypeKind.USER); + assertEquals(0, list.size()); + + list = searchDAO.findByDerAttrValue( + derSchemaDAO.findById("cn").orElseThrow().getExpression(), "VIVALDI, ANTONIO", true, AnyTypeKind.USER); + assertEquals(1, list.size()); + } + + @Test + public void findByInvalidDerAttrValue() { + assertTrue(searchDAO.findByDerAttrValue( + derSchemaDAO.findById("cn").orElseThrow().getExpression(), + "Antonio, Maria, Rossi", false, AnyTypeKind.USER).isEmpty()); + } + + @Test + public void findByInvalidDerAttrExpression() { + assertThrows(IllegalArgumentException.class, () -> searchDAO.findByDerAttrValue( + derSchemaDAO.findById("noschema").orElseThrow().getExpression(), + "Antonio, Maria", false, AnyTypeKind.USER).isEmpty()); + } + @Test public void issue202() { ResourceCond ws2 = new ResourceCond(); diff --git a/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/MultitenancyTest.java b/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/MultitenancyTest.java index d1531ffd8b7..ab83a95e8c1 100644 --- a/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/MultitenancyTest.java +++ b/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/MultitenancyTest.java @@ -35,7 +35,6 @@ import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.domain.Pageable; import org.springframework.test.annotation.DirtiesContext; import org.springframework.transaction.annotation.Transactional; @@ -74,10 +73,10 @@ public void readPlainSchemas() { public void readRealm() { assertEquals( 1, - realmSearchDAO.findDescendants(realmDAO.getRoot().getFullPath(), null, Pageable.unpaged()).size()); + realmSearchDAO.findDescendants(realmDAO.getRoot().getFullPath(), null).size()); assertEquals( realmDAO.getRoot(), - realmSearchDAO.findDescendants(realmDAO.getRoot().getFullPath(), null, Pageable.unpaged()).getFirst()); + realmSearchDAO.findDescendants(realmDAO.getRoot().getFullPath(), null).getFirst()); } @Test diff --git a/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/RealmTest.java b/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/RealmTest.java index 5dddbdb6cb0..55453d5d9a0 100644 --- a/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/RealmTest.java +++ b/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/RealmTest.java @@ -98,14 +98,14 @@ public void findChildren() { @Test public void findAll() { - List list = realmSearchDAO.findDescendants(realmDAO.getRoot().getFullPath(), null, Pageable.unpaged()); + List list = realmSearchDAO.findDescendants(realmDAO.getRoot().getFullPath(), null); assertNotNull(list); assertFalse(list.isEmpty()); list.forEach(Assertions::assertNotNull); assertEquals(4, realmDAO.findAll(Pageable.unpaged()).stream().count()); - list = realmSearchDAO.findDescendants(Set.of("/even", "/odd"), null, Pageable.unpaged()); + list = realmSearchDAO.search(Set.of("/even", "/odd"), realmSearchDAO.getAllMatchingCond(), Pageable.unpaged()); assertEquals(3, list.size()); assertNotNull(list.stream().filter(realm -> "even".equals(realm.getName())).findFirst().orElseThrow()); assertNotNull(list.stream().filter(realm -> "two".equals(realm.getName())).findFirst().orElseThrow()); diff --git a/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/UserTest.java b/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/UserTest.java index ad8a4ea9777..458d0206281 100644 --- a/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/UserTest.java +++ b/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/UserTest.java @@ -30,7 +30,6 @@ import java.util.List; import org.apache.syncope.common.lib.types.CipherAlgorithm; import org.apache.syncope.core.persistence.api.EncryptorManager; -import org.apache.syncope.core.persistence.api.dao.DerSchemaDAO; import org.apache.syncope.core.persistence.api.dao.ExternalResourceDAO; import org.apache.syncope.core.persistence.api.dao.RealmDAO; import org.apache.syncope.core.persistence.api.dao.RealmSearchDAO; @@ -62,9 +61,6 @@ public class UserTest extends AbstractTest { @Autowired private ExternalResourceDAO resourceDAO; - @Autowired - private DerSchemaDAO derSchemaDAO; - @Autowired private SecurityQuestionDAO securityQuestionDAO; @@ -110,33 +106,6 @@ public void count() { assertEquals(5, count); } - @Test - public void findByDerAttrValue() { - List list = userDAO.findByDerAttrValue( - derSchemaDAO.findById("cn").orElseThrow().getExpression(), "Vivaldi, Antonio", false); - assertEquals(1, list.size()); - - list = userDAO.findByDerAttrValue( - derSchemaDAO.findById("cn").orElseThrow().getExpression(), "VIVALDI, ANTONIO", false); - assertEquals(0, list.size()); - - list = userDAO.findByDerAttrValue( - derSchemaDAO.findById("cn").orElseThrow().getExpression(), "VIVALDI, ANTONIO", true); - assertEquals(1, list.size()); - } - - @Test - public void findByInvalidDerAttrValue() { - assertTrue(userDAO.findByDerAttrValue( - derSchemaDAO.findById("cn").orElseThrow().getExpression(), "Antonio, Maria, Rossi", false).isEmpty()); - } - - @Test - public void findByInvalidDerAttrExpression() { - assertThrows(IllegalArgumentException.class, () -> userDAO.findByDerAttrValue( - derSchemaDAO.findById("noschema").orElseThrow().getExpression(), "Antonio, Maria", false).isEmpty()); - } - @Test public void findByKey() { assertTrue(userDAO.findById("1417acbe-cbf6-4277-9372-e75e04f97000").isPresent()); diff --git a/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/outer/AnySearchTest.java b/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/outer/AnySearchTest.java index 968e110d475..307d25e5e0f 100644 --- a/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/outer/AnySearchTest.java +++ b/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/outer/AnySearchTest.java @@ -34,6 +34,7 @@ import org.apache.syncope.core.persistence.api.attrvalue.PlainAttrValidationManager; import org.apache.syncope.core.persistence.api.dao.AnyObjectDAO; import org.apache.syncope.core.persistence.api.dao.AnySearchDAO; +import org.apache.syncope.core.persistence.api.dao.DerSchemaDAO; import org.apache.syncope.core.persistence.api.dao.GroupDAO; import org.apache.syncope.core.persistence.api.dao.RealmDAO; import org.apache.syncope.core.persistence.api.dao.RealmSearchDAO; @@ -43,6 +44,7 @@ import org.apache.syncope.core.persistence.api.dao.search.AttrCond; 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.DerSchema; import org.apache.syncope.core.persistence.api.entity.PlainAttr; import org.apache.syncope.core.persistence.api.entity.Role; import org.apache.syncope.core.persistence.api.entity.anyobject.AMembership; @@ -82,6 +84,9 @@ public class AnySearchTest extends AbstractTest { @Autowired private RoleDAO roleDAO; + @Autowired + private DerSchemaDAO derSchemaDAO; + @Autowired private PlainAttrValidationManager validator; @@ -199,6 +204,43 @@ public void issueSYNCOPE95() { assertEquals("c9b2dec2-00a7-4855-97c0-d854842b4b24", users.getFirst().getKey()); } + @Test + public void issueSYNCOPE800() { + // create derived attribute (literal as prefix) + DerSchema prefix = entityFactory.newEntity(DerSchema.class); + prefix.setKey("kprefix"); + prefix.setExpression("'k' + firstname"); + + derSchemaDAO.save(prefix); + entityManager.flush(); + + // create derived attribute (literal as suffix) + DerSchema suffix = entityFactory.newEntity(DerSchema.class); + suffix.setKey("ksuffix"); + suffix.setExpression("firstname + 'k'"); + + derSchemaDAO.save(suffix); + entityManager.flush(); + + // add derived attributes to user + User owner = userDAO.findByUsername("vivaldi").orElseThrow(); + + String firstname = owner.getPlainAttr("firstname").get().getValuesAsStrings().getFirst(); + assertNotNull(firstname); + + // search by ksuffix derived attribute + List list = searchDAO.findByDerAttrValue( + derSchemaDAO.findById("ksuffix").orElseThrow().getExpression(), + firstname + 'k', false, AnyTypeKind.USER); + assertEquals(1, list.size()); + + // search by kprefix derived attribute + list = searchDAO.findByDerAttrValue( + derSchemaDAO.findById("kprefix").orElseThrow().getExpression(), + 'k' + firstname, false, AnyTypeKind.USER); + assertEquals(1, list.size()); + } + @Test public void issueSYNCOPE1417() { AnyCond usernameLeafCond = new AnyCond(AnyCond.Type.EQ); diff --git a/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/outer/RealmTest.java b/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/outer/RealmTest.java index ad9bb45f728..be75f4cbd79 100644 --- a/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/outer/RealmTest.java +++ b/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/outer/RealmTest.java @@ -18,22 +18,33 @@ */ package org.apache.syncope.core.persistence.jpa.outer; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.util.List; +import java.util.Set; +import org.apache.syncope.common.lib.SyncopeConstants; import org.apache.syncope.core.persistence.api.attrvalue.PlainAttrValidationManager; import org.apache.syncope.core.persistence.api.dao.AnyTypeClassDAO; import org.apache.syncope.core.persistence.api.dao.GroupDAO; import org.apache.syncope.core.persistence.api.dao.RealmDAO; import org.apache.syncope.core.persistence.api.dao.RealmSearchDAO; import org.apache.syncope.core.persistence.api.dao.RoleDAO; +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.PlainAttr; import org.apache.syncope.core.persistence.api.entity.Realm; import org.apache.syncope.core.persistence.api.entity.Role; import org.apache.syncope.core.persistence.api.entity.group.Group; import org.apache.syncope.core.persistence.jpa.AbstractTest; +import org.apache.syncope.core.persistence.jpa.entity.AbstractAttributable; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; import org.springframework.transaction.annotation.Transactional; @Transactional @@ -105,4 +116,126 @@ public void delete() { role = roleDAO.findById("User reviewer").orElseThrow(); assertEquals(beforeSize - 1, role.getRealms().size()); } + + @Test + public void search() { + Realm two = realmSearchDAO.findByFullPath("/even/two").orElseThrow(); + two.add(anyTypeClassDAO.findById("other").orElseThrow()); + two = realmDAO.save(two); + entityManager.flush(); + + two = realmDAO.findById(two.getKey()).orElseThrow(); + PlainAttr aLong = new PlainAttr(); + aLong.setSchema("aLong"); + aLong.add(validator, "42"); + two.add(aLong); + ((AbstractAttributable) two).list2json(); + two = realmDAO.save(two); + entityManager.flush(); + + Realm odd = realmSearchDAO.findByFullPath("/odd").orElseThrow(); + odd.add(anyTypeClassDAO.findById("other").orElseThrow()); + odd = realmDAO.save(odd); + entityManager.flush(); + + odd = realmDAO.findById(odd.getKey()).orElseThrow(); + PlainAttr oddLong = new PlainAttr(); + oddLong.setSchema("aLong"); + oddLong.add(validator, "99"); + odd.add(oddLong); + ((AbstractAttributable) odd).list2json(); + realmDAO.save(odd); + entityManager.flush(); + + two = realmDAO.findById(two.getKey()).orElseThrow(); + assertEquals(anyTypeClassDAO.findById("other").orElseThrow(), two.getAnyTypeClasses().iterator().next()); + assertEquals(1, two.getPlainAttrs().size()); + assertEquals(42, two.getPlainAttr("aLong").orElseThrow().getValues().getFirst().getLongValue()); + + odd = realmDAO.findById(odd.getKey()).orElseThrow(); + assertEquals(anyTypeClassDAO.findById("other").orElseThrow(), odd.getAnyTypeClasses().iterator().next()); + assertEquals(1, odd.getPlainAttrs().size()); + assertEquals(99, odd.getPlainAttr("aLong").orElseThrow().getValues().getFirst().getLongValue()); + + AnyCond name = new AnyCond(AttrCond.Type.EQ); + name.setSchema("name"); + name.setExpression("two"); + + List result = realmSearchDAO.search( + Set.of(SyncopeConstants.ROOT_REALM), + SearchCond.of(name), + Pageable.unpaged()); + assertEquals(1, result.size()); + assertEquals("two", result.getFirst().getName()); + + AttrCond attrEq = new AttrCond(AttrCond.Type.EQ); + attrEq.setSchema("aLong"); + attrEq.setExpression("42"); + + result = realmSearchDAO.search( + Set.of(SyncopeConstants.ROOT_REALM), + SearchCond.of(attrEq), + Pageable.unpaged()); + assertEquals(1, result.size()); + assertEquals("two", result.getFirst().getName()); + + result = realmSearchDAO.search( + Set.of(SyncopeConstants.ROOT_REALM), + SearchCond.and(SearchCond.of(name), SearchCond.of(attrEq)), + Pageable.unpaged()); + assertEquals(1, result.size()); + assertEquals("two", result.getFirst().getName()); + + AttrCond attrEq2 = new AttrCond(AttrCond.Type.EQ); + attrEq2.setSchema("aLong"); + attrEq2.setExpression("99"); + + result = realmSearchDAO.search( + Set.of(SyncopeConstants.ROOT_REALM), + SearchCond.or(SearchCond.of(attrEq), SearchCond.of(attrEq2)), + Pageable.unpaged()); + assertTrue(result.stream().anyMatch(r -> "two".equals(r.getName()))); + assertTrue(result.stream().anyMatch(r -> "odd".equals(r.getName()))); + + AttrCond attrIsNull = new AttrCond(AttrCond.Type.ISNULL); + attrIsNull.setSchema("aLong"); + + result = realmSearchDAO.search( + Set.of(SyncopeConstants.ROOT_REALM), + SearchCond.of(attrIsNull), + Pageable.unpaged()); + result.forEach(r -> assertFalse("two".equals(r.getName()) || "odd".equals(r.getName()))); + + AttrCond attrIsNotNull = new AttrCond(AttrCond.Type.ISNOTNULL); + attrIsNotNull.setSchema("aLong"); + + assertEquals(2, realmSearchDAO.count( + Set.of(SyncopeConstants.ROOT_REALM), + SearchCond.of(attrIsNotNull))); + + result = realmSearchDAO.search( + Set.of(SyncopeConstants.ROOT_REALM), + SearchCond.of(attrIsNotNull), + Pageable.unpaged(Sort.by(new Sort.Order(Sort.Direction.ASC, "name")))); + assertEquals("odd", result.get(0).getName()); + assertEquals("two", result.get(1).getName()); + + result = realmSearchDAO.search( + Set.of(SyncopeConstants.ROOT_REALM), + SearchCond.of(attrIsNotNull), + Pageable.unpaged(Sort.by(new Sort.Order(Sort.Direction.ASC, "aLong")))); + assertEquals("two", result.get(0).getName()); + assertEquals("odd", result.get(1).getName()); + + AttrCond cond1 = new AttrCond(AttrCond.Type.EQ); + cond1.setSchema("aLong"); + cond1.setExpression("42"); + AttrCond cond2 = new AttrCond(AttrCond.Type.IEQ); + cond2.setSchema("ctype"); + cond2.setExpression("string"); + assertDoesNotThrow(() -> realmSearchDAO.search( + Set.of(SyncopeConstants.ROOT_REALM), + SearchCond.and(SearchCond.of(cond1), SearchCond.of(cond2)), + Pageable.unpaged())); + } } diff --git a/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/outer/UserTest.java b/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/outer/UserTest.java index b4bb2186df4..c1b78e430e8 100644 --- a/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/outer/UserTest.java +++ b/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/outer/UserTest.java @@ -32,7 +32,6 @@ import org.apache.syncope.core.persistence.api.attrvalue.PlainAttrValidationManager; import org.apache.syncope.core.persistence.api.dao.AnyObjectDAO; import org.apache.syncope.core.persistence.api.dao.DelegationDAO; -import org.apache.syncope.core.persistence.api.dao.DerSchemaDAO; import org.apache.syncope.core.persistence.api.dao.ExternalResourceDAO; import org.apache.syncope.core.persistence.api.dao.GroupDAO; import org.apache.syncope.core.persistence.api.dao.PlainSchemaDAO; @@ -40,7 +39,6 @@ import org.apache.syncope.core.persistence.api.dao.RoleDAO; import org.apache.syncope.core.persistence.api.dao.UserDAO; import org.apache.syncope.core.persistence.api.entity.Delegation; -import org.apache.syncope.core.persistence.api.entity.DerSchema; import org.apache.syncope.core.persistence.api.entity.PlainAttr; import org.apache.syncope.core.persistence.api.entity.Role; import org.apache.syncope.core.persistence.api.entity.user.LinkedAccount; @@ -74,9 +72,6 @@ public class UserTest extends AbstractTest { @Autowired private PlainSchemaDAO plainSchemaDAO; - @Autowired - private DerSchemaDAO derSchemaDAO; - @Autowired private ExternalResourceDAO resourceDAO; @@ -242,42 +237,4 @@ public void deleteCascadeOnDelegations() { assertTrue(delegationDAO.findById(delegation.getKey()).isEmpty()); } - - /** - * Search by derived attribute. - */ - @Test - public void issueSYNCOPE800() { - // create derived attribute (literal as prefix) - DerSchema prefix = entityFactory.newEntity(DerSchema.class); - prefix.setKey("kprefix"); - prefix.setExpression("'k' + firstname"); - - derSchemaDAO.save(prefix); - entityManager.flush(); - - // create derived attribute (literal as suffix) - DerSchema suffix = entityFactory.newEntity(DerSchema.class); - suffix.setKey("ksuffix"); - suffix.setExpression("firstname + 'k'"); - - derSchemaDAO.save(suffix); - entityManager.flush(); - - // add derived attributes to user - User owner = userDAO.findByUsername("vivaldi").orElseThrow(); - - String firstname = owner.getPlainAttr("firstname").get().getValuesAsStrings().getFirst(); - assertNotNull(firstname); - - // search by ksuffix derived attribute - List list = userDAO.findByDerAttrValue( - derSchemaDAO.findById("ksuffix").orElseThrow().getExpression(), firstname + 'k', false); - assertEquals(1, list.size()); - - // search by kprefix derived attribute - list = userDAO.findByDerAttrValue( - derSchemaDAO.findById("kprefix").orElseThrow().getExpression(), 'k' + firstname, false); - assertEquals(1, list.size()); - } } diff --git a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/PersistenceContext.java b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/PersistenceContext.java index e3d3afdfadb..316a7454f4f 100644 --- a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/PersistenceContext.java +++ b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/PersistenceContext.java @@ -84,10 +84,10 @@ import org.apache.syncope.core.persistence.api.entity.EntityFactory; import org.apache.syncope.core.persistence.api.entity.PlainAttr; import org.apache.syncope.core.persistence.api.entity.task.TaskUtilsFactory; -import org.apache.syncope.core.persistence.api.search.SearchCondVisitor; +import org.apache.syncope.core.persistence.api.search.AnySearchCondVisitor; +import org.apache.syncope.core.persistence.api.utils.RealmUtils; import org.apache.syncope.core.persistence.common.CommonPersistenceContext; import org.apache.syncope.core.persistence.common.RuntimeDomainLoader; -import org.apache.syncope.core.persistence.common.dao.AnyFinder; import org.apache.syncope.core.persistence.neo4j.content.XMLContentExporter; import org.apache.syncope.core.persistence.neo4j.content.XMLContentLoader; import org.apache.syncope.core.persistence.neo4j.dao.Neo4jAnyMatchDAO; @@ -437,12 +437,6 @@ public SyncopeNeo4jRepositoryFactory neo4jRepositoryFactory( return new SyncopeNeo4jRepositoryFactory(neo4jOperations, mappingContext); } - @ConditionalOnMissingBean - @Bean - public AnyFinder anyFinder(final @Lazy PlainSchemaDAO plainSchemaDAO, final @Lazy AnySearchDAO anySearchDAO) { - return new AnyFinder(plainSchemaDAO, anySearchDAO); - } - @ConditionalOnMissingBean @Bean public AccessTokenDAO accessTokenDAO(final SyncopeNeo4jRepositoryFactory neo4jRepositoryFactory) { @@ -494,7 +488,6 @@ public AnyObjectRepoExt anyObjectRepoExt( final @Lazy DynRealmDAO dynRealmDAO, final @Lazy UserDAO userDAO, final @Lazy GroupDAO groupDAO, - final @Lazy AnyFinder anyFinder, final Neo4jTemplate neo4jTemplate, final Neo4jClient neo4jClient, final NodeValidator nodeValidator, @@ -509,7 +502,6 @@ public AnyObjectRepoExt anyObjectRepoExt( dynRealmDAO, userDAO, groupDAO, - anyFinder, neo4jTemplate, neo4jClient, nodeValidator, @@ -826,7 +818,7 @@ public DynRealmRepoExt dynRealmRepoExt( final @Lazy AnyObjectDAO anyObjectDAO, final AnySearchDAO anySearchDAO, final AnyMatchDAO anyMatchDAO, - final SearchCondVisitor searchCondVisitor, + final AnySearchCondVisitor searchCondVisitor, final Neo4jTemplate neo4jTemplate, final Neo4jClient neo4jClient, final NodeValidator nodeValidator) { @@ -926,8 +918,7 @@ public GroupRepoExt groupRepoExt( final @Lazy UserDAO userDAO, final @Lazy AnyObjectDAO anyObjectDAO, final AnySearchDAO anySearchDAO, - final @Lazy AnyFinder anyFinder, - final SearchCondVisitor searchCondVisitor, + final AnySearchCondVisitor searchCondVisitor, final Neo4jTemplate neo4jTemplate, final Neo4jClient neo4jClient, final NodeValidator nodeValidator, @@ -946,7 +937,6 @@ public GroupRepoExt groupRepoExt( userDAO, anyObjectDAO, anySearchDAO, - anyFinder, searchCondVisitor, neo4jTemplate, neo4jClient, @@ -1175,11 +1165,22 @@ public RealmDAO realmDAO( @ConditionalOnMissingBean @Bean public RealmSearchDAO realmSearchDAO( + final @Lazy RealmDAO realmDAO, + final PlainSchemaDAO plainSchemaDAO, + final EntityFactory entityFactory, + final PlainAttrValidationManager validator, + final RealmUtils realmUtils, final Neo4jTemplate neo4jTemplate, - final Neo4jClient neo4jClient, - final Cache realmCache) { + final Neo4jClient neo4jClient) { - return new Neo4jRealmSearchDAO(neo4jTemplate, neo4jClient, realmCache); + return new Neo4jRealmSearchDAO( + realmDAO, + plainSchemaDAO, + entityFactory, + validator, + realmUtils, + neo4jTemplate, + neo4jClient); } @ConditionalOnMissingBean @@ -1297,7 +1298,7 @@ public RoleRepoExt roleRepoExt( final @Lazy AnyMatchDAO anyMatchDAO, final @Lazy AnySearchDAO anySearchDAO, final DelegationDAO delegationDAO, - final SearchCondVisitor searchCondVisitor, + final AnySearchCondVisitor searchCondVisitor, final Neo4jTemplate neo4jTemplate, final Neo4jClient neo4jClient, final NodeValidator nodeValidator, @@ -1432,7 +1433,6 @@ public UserRepoExt userRepoExt( final @Lazy GroupDAO groupDAO, final DelegationDAO delegationDAO, final FIQLQueryDAO fiqlQueryDAO, - final @Lazy AnyFinder anyFinder, final Neo4jTemplate neo4jTemplate, final Neo4jClient neo4jClient, final NodeValidator nodeValidator, @@ -1450,7 +1450,6 @@ public UserRepoExt userRepoExt( groupDAO, delegationDAO, fiqlQueryDAO, - anyFinder, securityProperties, neo4jTemplate, neo4jClient, diff --git a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jAnySearchDAO.java b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jAnySearchDAO.java index 85c9cf58ca3..728e3a1ff0d 100644 --- a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jAnySearchDAO.java +++ b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jAnySearchDAO.java @@ -28,7 +28,6 @@ import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; -import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.Strings; import org.apache.commons.text.TextStringBuilder; @@ -137,6 +136,20 @@ protected static String escapeIfString(final String value, final boolean isStr) : value; } + protected static void queryOp( + final TextStringBuilder query, + final String op, + final QueryInfo leftInfo, + final QueryInfo rightInfo) { + + query.append("WHERE EXISTS { "). + append(Strings.CS.prependIfMissing(leftInfo.query().toString(), "MATCH (n) ")). + append(" } "). + append(op).append(" EXISTS { "). + append(Strings.CS.prependIfMissing(rightInfo.query().toString(), "MATCH (n) ")). + append(" }"); + } + protected final Neo4jTemplate neo4jTemplate; protected final Neo4jClient neo4jClient; @@ -206,7 +219,8 @@ protected AdminRealmsFilter getAdminRealmsFilter( return noRealm; }); - realmKeys.addAll(realmSearchDAO.findDescendants(realm.getFullPath(), base.getFullPath())); + realmKeys.addAll(realmSearchDAO.findDescendants(realm.getFullPath(), base.getFullPath()). + stream().map(Realm::getKey).toList()); } else { dynRealmDAO.findById(realmPath).ifPresentOrElse( dynRealm -> dynRealmKeys.add(dynRealm.getKey()), @@ -647,12 +661,13 @@ protected AnyCondQuery getQuery( null); } - CheckResult checked = check(cond, kind); - - if (ArrayUtils.contains( - RELATIONSHIP_FIELDS, - StringUtils.substringBefore(checked.cond().getSchema(), "_id"))) { + CheckResult checked = check( + cond, + anyUtilsFactory.getInstance(kind).getField(cond.getSchema()). + orElseThrow(() -> new IllegalArgumentException("Invalid schema " + cond.getSchema())), + RELATIONSHIP_FIELDS); + if (RELATIONSHIP_FIELDS.contains(StringUtils.substringBefore(checked.cond().getSchema(), "_id"))) { String field = StringUtils.substringBefore(checked.cond().getSchema(), "_id"); switch (field) { case "userOwner" -> { @@ -715,20 +730,6 @@ protected void getQueryForCustomConds( // do nothing by default, leave it open for subclasses } - protected void queryOp( - final TextStringBuilder query, - final String op, - final QueryInfo leftInfo, - final QueryInfo rightInfo) { - - query.append("WHERE EXISTS { "). - append(Strings.CS.prependIfMissing(leftInfo.query().toString(), "MATCH (n) ")). - append(" } "). - append(op).append(" EXISTS { "). - append(Strings.CS.prependIfMissing(rightInfo.query().toString(), "MATCH (n) ")). - append(" }"); - } - protected QueryInfo getQuery(final AnyTypeKind kind, final SearchCond cond, final Map parameters) { boolean not = cond.getType() == SearchCond.Type.NOT_LEAF; @@ -970,6 +971,8 @@ protected long doCount( // 4. prepare the count query query.append("RETURN COUNT(id)"); + LOG.debug("Query: {}, parameters: {}", query, parameters); + return neo4jTemplate.count(query.toString(), parameters); } @@ -1046,7 +1049,7 @@ protected List doSearch( append(" LIMIT ").append(pageable.getPageSize()); } - LOG.debug("Query with auth and order by statements: {}, parameters: {}", query, parameters); + LOG.debug("Query: {}, parameters: {}", query, parameters); // 5. Prepare the result (avoiding duplicates) return buildResult(neo4jClient.query(query.toString()).bindAll(parameters).fetch().all().stream(). diff --git a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jRealmDAO.java b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jRealmDAO.java index d09da509497..17d059ed3aa 100644 --- a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jRealmDAO.java +++ b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jRealmDAO.java @@ -259,7 +259,7 @@ public void delete(final Realm realm) { return; } - realmSearchDAO.findDescendants(realm.getFullPath(), null, Pageable.unpaged()).forEach(toBeDeleted -> { + realmSearchDAO.findDescendants(realm.getFullPath(), null).forEach(toBeDeleted -> { roleDAO.findByRealms(toBeDeleted).forEach(role -> role.getRealms().remove(toBeDeleted)); cascadeDelete( diff --git a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jRealmSearchDAO.java b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jRealmSearchDAO.java index 753879b62df..870a0923442 100644 --- a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jRealmSearchDAO.java +++ b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jRealmSearchDAO.java @@ -18,62 +18,125 @@ */ package org.apache.syncope.core.persistence.neo4j.dao; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Collection; 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.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; -import javax.cache.Cache; +import java.util.stream.Stream; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Strings; +import org.apache.commons.text.TextStringBuilder; +import org.apache.syncope.common.lib.SyncopeClientException; import org.apache.syncope.common.lib.SyncopeConstants; +import org.apache.syncope.common.lib.types.AttrSchemaType; +import org.apache.syncope.common.lib.types.ClientExceptionType; +import org.apache.syncope.core.persistence.api.attrvalue.PlainAttrValidationManager; import org.apache.syncope.core.persistence.api.dao.MalformedPathException; +import org.apache.syncope.core.persistence.api.dao.PlainSchemaDAO; import org.apache.syncope.core.persistence.api.dao.RealmDAO; -import org.apache.syncope.core.persistence.api.dao.RealmSearchDAO; +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.EntityFactory; +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.persistence.neo4j.entity.EntityCacheKey; +import org.apache.syncope.core.persistence.api.utils.RealmUtils; +import org.apache.syncope.core.persistence.common.dao.AbstractRealmSearchDAO; +import org.apache.syncope.core.persistence.neo4j.dao.repo.AnyRepoExt; import org.apache.syncope.core.persistence.neo4j.entity.Neo4jRealm; import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Sort.Order; import org.springframework.data.neo4j.core.Neo4jClient; import org.springframework.data.neo4j.core.Neo4jTemplate; +import org.springframework.data.util.Streamable; import org.springframework.transaction.annotation.Transactional; -public class Neo4jRealmSearchDAO extends AbstractDAO implements RealmSearchDAO { +public class Neo4jRealmSearchDAO extends AbstractRealmSearchDAO { - protected static StringBuilder buildDescendantsQuery( - final Set bases, - final String keyword, - final Map parameters) { + protected record AnyCondQuery(String query, String field) { - AtomicInteger index = new AtomicInteger(0); - String basesClause = bases.stream().map(base -> { - int idx = index.incrementAndGet(); - parameters.put("base" + idx, base); - parameters.put("like" + idx, SyncopeConstants.ROOT_REALM.equals(base) ? "/.*" : base + "/.*"); - return "n.fullPath = $base" + idx + " OR n.fullPath =~ $like" + idx; - }).collect(Collectors.joining(" OR ")); + } - StringBuilder queryString = new StringBuilder("MATCH (n:").append(Neo4jRealm.NODE).append(") "). - append("WHERE (").append(basesClause).append(')'); + protected record AttrCondQuery(String query, PlainSchema schema) { - if (keyword != null) { - queryString.append(" AND toLower(n.name) =~ $name"); - parameters.put("name", keyword.replace("%", ".*").replaceAll("_", "\\\\_").toLowerCase() + ".*"); + } + + protected record QueryInfo( + TextStringBuilder query, + Set fields, + Set plainSchemas) { + + } + + protected static String setParameter(final Map parameters, final Object parameter) { + String name = "param" + parameters.size(); + parameters.put(name, parameter); + return name; + } + + protected static void appendPlainAttrCond( + final TextStringBuilder query, final PlainSchema schema, final String cond) { + + if (schema.isUniqueConstraint()) { + query.append(schema.getKey()).append('.').append(key(schema.getType())).append(cond); + } else { + query.append("any(k IN ").append(schema.getKey()). + append(" WHERE k").append('.').append(key(schema.getType())).append(cond). + append(")"); } + } - return queryString; + protected static String escapeIfString(final String value, final boolean isStr) { + return isStr + ? new StringBuilder().append('"').append(value).append('"').toString() + : value; } - protected final Cache cache; + protected static void queryOp( + final TextStringBuilder query, + final String op, + final QueryInfo leftInfo, + final QueryInfo rightInfo) { + + query.append("WHERE EXISTS { "). + append(Strings.CS.prependIfMissing(leftInfo.query().toString(), "MATCH (n) ")). + append(" } "). + append(op).append(" EXISTS { "). + append(Strings.CS.prependIfMissing(rightInfo.query().toString(), "MATCH (n) ")). + append(" }"); + } + + protected final RealmDAO realmDAO; + + protected final RealmUtils realmUtils; + + protected final Neo4jTemplate neo4jTemplate; + + protected final Neo4jClient neo4jClient; public Neo4jRealmSearchDAO( + final RealmDAO realmDAO, + final PlainSchemaDAO plainSchemaDAO, + final EntityFactory entityFactory, + final PlainAttrValidationManager validator, + final RealmUtils realmUtils, final Neo4jTemplate neo4jTemplate, - final Neo4jClient neo4jClient, - final Cache cache) { + final Neo4jClient neo4jClient) { - super(neo4jTemplate, neo4jClient); - this.cache = cache; + super(plainSchemaDAO, entityFactory, validator); + this.realmDAO = realmDAO; + this.realmUtils = realmUtils; + this.neo4jTemplate = neo4jTemplate; + this.neo4jClient = neo4jClient; } @Transactional(readOnly = true) @@ -89,68 +152,518 @@ public Optional findByFullPath(final String fullPath) { return neo4jClient.query( "MATCH (n:" + Neo4jRealm.NODE + ") WHERE n.fullPath = $fullPath RETURN n.id"). bindAll(Map.of("fullPath", fullPath)).fetch().one(). - flatMap(toOptional("n.id", Neo4jRealm.class, cache)); + flatMap(found -> realmDAO.findById(found.get("n.id").toString()).map(n -> (Realm) n)); + } + + protected List toList( + final Collection> result, + final String property) { + + return result.stream(). + map(found -> realmDAO.findById(found.get(property).toString())). + flatMap(Optional::stream).map(n -> (Realm) n).toList(); } @Override public List findByName(final String name) { return toList(neo4jClient.query( "MATCH (n:" + Neo4jRealm.NODE + ") WHERE n.name = $name RETURN n.id"). - bindAll(Map.of("name", name)).fetch().all(), "n.id", Neo4jRealm.class, cache); + bindAll(Map.of("name", name)).fetch().all(), "n.id"); } @Override public List findChildren(final Realm realm) { return toList(neo4jClient.query( "MATCH (n:" + Neo4jRealm.NODE + " {id: $id})<-[r:" + Neo4jRealm.PARENT_REL + "]-(c) RETURN c.id"). - bindAll(Map.of("id", realm.getKey())).fetch().all(), "c.id", Neo4jRealm.class, cache); + bindAll(Map.of("id", realm.getKey())).fetch().all(), "c.id"); } @Override - public long countDescendants(final String base, final String keyword) { - return countDescendants(Set.of(base), keyword); + public List findDescendants(final String base, final String prefix) { + Map parameters = new HashMap<>(); + + StringBuilder query = new StringBuilder("MATCH (n:").append(Neo4jRealm.NODE).append(") "). + append("WHERE (").append("n.fullPath = $base OR n.fullPath =~ $like").append(')'); + parameters.put("base", base); + parameters.put("like", SyncopeConstants.ROOT_REALM.equals(base) ? "/.*" : base + "/.*"); + + if (prefix != null) { + query.append(" AND (n.fullPath = $prefix OR n.fullPath =~ $likePrefix)"); + parameters.put("prefix", prefix); + parameters.put("likePrefix", SyncopeConstants.ROOT_REALM.equals(prefix) ? "/.*" : prefix + "/.*"); + } + + query.append(" RETURN n.id ORDER BY n.fullPath"); + + return toList(neo4jClient.query( + query.toString()).bindAll(parameters).fetch().all(), "n.id"); } - @Override - public long countDescendants(final Set bases, final String keyword) { - Map parameters = new HashMap<>(); + protected QueryInfo getQuery(final SearchCond cond, final Map parameters) { + boolean not = cond.getType() == SearchCond.Type.NOT_LEAF; + + TextStringBuilder query = new TextStringBuilder(); + Set involvedFields = new HashSet<>(); + Set involvedPlainSchemas = new HashSet<>(); - StringBuilder queryString = buildDescendantsQuery(bases, keyword, parameters).append(" RETURN COUNT(n)"); - return neo4jTemplate.count(queryString.toString(), parameters); + switch (cond.getType()) { + case LEAF, NOT_LEAF -> { + cond.asLeaf(AnyCond.class).ifPresentOrElse( + anyCond -> { + AnyCondQuery anyCondQuery = getQuery(anyCond, not, parameters); + query.append(anyCondQuery.query()); + Optional.ofNullable(anyCondQuery.field()).ifPresent(involvedFields::add); + }, + () -> cond.asLeaf(AttrCond.class).ifPresent(leaf -> { + AttrCondQuery attrCondQuery = getQuery(leaf, not, parameters); + query.append(attrCondQuery.query()); + involvedPlainSchemas.add(attrCondQuery.schema()); + })); + + // allow for additional search conditions + getQueryForCustomConds(cond, parameters, not, query); + } + case AND -> { + QueryInfo leftAndInfo = getQuery(cond.getLeft(), parameters); + involvedFields.addAll(leftAndInfo.fields()); + involvedPlainSchemas.addAll(leftAndInfo.plainSchemas()); + + QueryInfo rigthAndInfo = getQuery(cond.getRight(), parameters); + involvedFields.addAll(rigthAndInfo.fields()); + involvedPlainSchemas.addAll(rigthAndInfo.plainSchemas()); + + queryOp(query, "AND", leftAndInfo, rigthAndInfo); + } + + case OR -> { + QueryInfo leftOrInfo = getQuery(cond.getLeft(), parameters); + involvedFields.addAll(leftOrInfo.fields()); + involvedPlainSchemas.addAll(leftOrInfo.plainSchemas()); + + QueryInfo rigthOrInfo = getQuery(cond.getRight(), parameters); + involvedFields.addAll(rigthOrInfo.fields()); + involvedPlainSchemas.addAll(rigthOrInfo.plainSchemas()); + + queryOp(query, "OR", leftOrInfo, rigthOrInfo); + } + + default -> { + } + } + + return new QueryInfo(query, involvedFields, involvedPlainSchemas); } - @Override - public List findDescendants(final String base, final String keyword, final Pageable pageable) { - return findDescendants(Set.of(base), keyword, pageable); + protected void wrapQuery( + final Set bases, + final QueryInfo queryInfo, + final Streamable orderBy, + final Map parameters) { + + TextStringBuilder match = new TextStringBuilder("MATCH (n:").append(Neo4jRealm.NODE).append(") "). + append("WITH n.id AS id"); + + // take fields into account + queryInfo.fields().remove("id"); + Stream.concat( + queryInfo.fields().stream(), + orderBy.stream().filter(clause -> !"id".equals(clause.getProperty()) + && realmUtils.getField(clause.getProperty()).isPresent()).map(Order::getProperty)). + distinct().forEach(field -> match.append(", n.").append(field).append(" AS ").append(field)); + + // take plain schemas into account + Stream.concat( + queryInfo.plainSchemas().stream(), + orderBy.stream().map(clause -> plainSchemaDAO.findById(clause.getProperty())). + flatMap(Optional::stream)).distinct().forEach(schema -> { + + match.append(", apoc.convert.getJsonProperty(n, 'plainAttrs.").append(schema.getKey()); + if (schema.isUniqueConstraint()) { + match.append("', '$.uniqueValue')"); + } else { + match.append("', '$.values')"); + } + match.append(" AS ").append(schema.getKey()); + }); + + TextStringBuilder query = queryInfo.query(); + + // take bases into account + AtomicInteger index = new AtomicInteger(0); + String basesClause = bases.stream().map(base -> { + int idx = index.incrementAndGet(); + parameters.put("base" + idx, base); + parameters.put("like" + idx, SyncopeConstants.ROOT_REALM.equals(base) ? "/.*" : base + "/.*"); + return "n.fullPath = $base" + idx + " OR n.fullPath =~ $like" + idx; + }).collect(Collectors.joining(" OR ")); + if (query.startsWith("MATCH (n)")) { + query.replaceFirst("MATCH (n)", match + " WHERE (EXISTS { MATCH (n)"); + query.append("} "); + } else { + query.replaceFirst("WHERE EXISTS", "WHERE (EXISTS"); + query.insert(0, match.append(' ')); + } + query.append(") AND EXISTS { ").append("(n) WHERE (").append(basesClause).append(")").append(" } "); + } + + protected AttrCondQuery getQuery( + final AttrCond cond, + final boolean not, + final Map parameters) { + + CheckResult checked = check(cond); + + TextStringBuilder query = new TextStringBuilder("MATCH (n) "); + switch (cond.getType()) { + case ISNOTNULL -> + query.append("WHERE n.`plainAttrs.").append(checked.schema().getKey()).append("` IS NOT NULL"); + + case ISNULL -> + query.append("WHERE n.`plainAttrs.").append(checked.schema().getKey()).append("` IS NULL"); + + default -> + fillAttrQuery(query, checked.value(), checked.schema(), cond, not, parameters); + } + + return new AttrCondQuery(query.toString(), checked.schema()); + } + + protected void getQueryForCustomConds( + final SearchCond cond, + final Map parameters, + final boolean not, + final TextStringBuilder query) { + + // do nothing by default, leave it open for subclasses + } + + protected void fillAttrQuery( + final TextStringBuilder query, + final PlainAttrValue attrValue, + final PlainSchema schema, + final AttrCond cond, + final boolean not, + final Map parameters) { + + if (not && cond.getType() == AttrCond.Type.ISNULL) { + cond.setType(AttrCond.Type.ISNOTNULL); + fillAttrQuery(query, attrValue, schema, cond, true, parameters); + return; + } + if (not) { + if (schema.isUniqueConstraint()) { + fillAttrQuery(query, attrValue, schema, cond, false, parameters); + query.replaceFirst("WHERE", "WHERE NOT("); + query.append(')'); + } else { + fillAttrQuery(query, attrValue, schema, cond, false, parameters); + query.replaceAll("any(", schema.getKey() + " IS NULL OR none("); + } + return; + } + + String value = Optional.ofNullable(attrValue.getDateValue()). + map(DateTimeFormatter.ISO_OFFSET_DATE_TIME::format). + orElseGet(cond::getExpression); + + boolean isStr = true; + boolean lower = false; + if (schema.getType().isStringClass()) { + lower = (cond.getType() == AttrCond.Type.IEQ || cond.getType() == AttrCond.Type.ILIKE); + } else if (schema.getType() != AttrSchemaType.Date) { + lower = false; + try { + switch (schema.getType()) { + case Long -> + Long.valueOf(value); + + case Double -> + Double.valueOf(value); + + case Boolean -> { + if (!("true".equalsIgnoreCase(value) || "false".equalsIgnoreCase(value))) { + throw new IllegalArgumentException(); + } + } + + default -> { + } + } + + isStr = false; + } catch (Exception nfe) { + // ignore + } + } + + query.append("WHERE "); + + switch (cond.getType()) { + case ISNULL -> { + } + + case ISNOTNULL -> + query.append(schema.getKey()).append(" IS NOT NULL"); + + case ILIKE, LIKE -> { + if (schema.getType().isStringClass()) { + appendPlainAttrCond( + query, + schema, + " =~ \"" + (lower ? "(?i)" : "") + + AnyRepoExt.escapeForLikeRegex(value).replace("%", ".*") + '"'); + } else { + query.append(ALWAYS_FALSE_CLAUSE); + LOG.error("LIKE is only compatible with string or enum schemas"); + } + } + + case IEQ, EQ -> { + if (StringUtils.containsAny(value, AnyRepoExt.REGEX_CHARS) || lower) { + appendPlainAttrCond( + query, + schema, + " =~ \"^" + (lower ? "(?i)" : "") + + AnyRepoExt.escapeForLikeRegex(value).replace("%", ".*") + "$\""); + } else { + appendPlainAttrCond( + query, + schema, + " = " + escapeIfString(value, isStr)); + } + } + + case GE -> + appendPlainAttrCond( + query, + schema, + " >= " + escapeIfString(value, isStr)); + + case GT -> + appendPlainAttrCond( + query, + schema, + " > " + escapeIfString(value, isStr)); + + case LE -> + appendPlainAttrCond( + query, + schema, + " <= " + escapeIfString(value, isStr)); + + case LT -> + appendPlainAttrCond( + query, + schema, + " < " + escapeIfString(value, isStr)); + + default -> { + } + } + // shouldn't occour: processed before + } + + protected void fillAttrQuery( + final TextStringBuilder query, + final PlainAttrValue attrValue, + final PlainSchema schema, + final AnyCond cond, + final boolean not, + final Map parameters) { + + if (not && cond.getType() == AttrCond.Type.ISNULL) { + cond.setType(AttrCond.Type.ISNOTNULL); + fillAttrQuery(query, attrValue, schema, cond, true, parameters); + return; + } + if (not) { + query.append("NOT ("); + fillAttrQuery(query, attrValue, schema, cond, false, parameters); + query.append(')'); + return; + } + if (not && cond.getType() == AttrCond.Type.ISNULL) { + cond.setType(AttrCond.Type.ISNOTNULL); + fillAttrQuery(query, attrValue, schema, cond, true, parameters); + return; + } + + boolean lower = schema.getType().isStringClass() + && (cond.getType() == AttrCond.Type.IEQ || cond.getType() == AttrCond.Type.ILIKE); + + String property = "n." + cond.getSchema(); + if (lower) { + property = "toLower (" + property + ')'; + } + + switch (cond.getType()) { + + case ISNULL -> + query.append(property).append(" IS NULL"); + + case ISNOTNULL -> + query.append(property).append(" IS NOT NULL"); + + case ILIKE, LIKE -> { + if (schema.getType().isStringClass()) { + query.append(property).append(" =~ "); + if (lower) { + query.append("toLower($"). + append(setParameter(parameters, cond.getExpression().replace("%", ".*"))). + append(')'); + } else { + query.append('$').append(setParameter(parameters, cond.getExpression().replace("%", ".*"))); + } + } else { + query.append(' ').append(ALWAYS_FALSE_CLAUSE); + LOG.error("LIKE is only compatible with string or enum schemas"); + } + } + + case IEQ, EQ -> { + query.append(property).append('='); + + if (lower) { + query.append("toLower($").append(setParameter(parameters, attrValue.getValue())).append(')'); + } else { + query.append('$').append(setParameter(parameters, attrValue.getValue())); + } + } + + case GE -> { + query.append(property); + if (not) { + query.append('<'); + } else { + query.append(">="); + } + query.append('$').append(setParameter(parameters, attrValue.getValue())); + } + + case GT -> { + query.append(property); + if (not) { + query.append("<="); + } else { + query.append('>'); + } + query.append('$').append(setParameter(parameters, attrValue.getValue())); + } + + case LE -> { + query.append(property); + if (not) { + query.append('>'); + } else { + query.append("<="); + } + query.append('$').append(setParameter(parameters, attrValue.getValue())); + } + + case LT -> { + query.append(property); + if (not) { + query.append(">="); + } else { + query.append('<'); + } + query.append('$').append(setParameter(parameters, attrValue.getValue())); + } + + default -> { + } + } + } + + protected AnyCondQuery getQuery( + final AnyCond cond, + final boolean not, + final Map parameters) { + + CheckResult checked = check( + cond, + realmUtils.getField(cond.getSchema()). + orElseThrow(() -> new IllegalArgumentException("Invalid schema " + cond.getSchema())), + RELATIONSHIP_FIELDS); + + TextStringBuilder query = new TextStringBuilder("MATCH (n) WHERE "); + + fillAttrQuery(query, checked.value(), checked.schema(), checked.cond(), not, parameters); + + return new AnyCondQuery(query.toString(), checked.cond().getSchema()); } @Override - public List findDescendants(final Set bases, final String keyword, final Pageable pageable) { + protected long doCount(final Set bases, final SearchCond cond) { Map parameters = new HashMap<>(); - StringBuilder queryString = buildDescendantsQuery(bases, keyword, parameters). - append(" RETURN n.id ORDER BY n.fullPath"); - if (pageable.isPaged()) { - queryString.append(" SKIP ").append(pageable.getPageSize() * pageable.getPageNumber()). - append(" LIMIT ").append(pageable.getPageSize()); - } + QueryInfo queryInfo = getQuery(cond, parameters); - return toList(neo4jClient.query( - queryString.toString()).bindAll(parameters).fetch().all(), "n.id", Neo4jRealm.class, cache); + wrapQuery(bases, queryInfo, Streamable.empty(), parameters); + TextStringBuilder query = queryInfo.query(); + + query.append("RETURN COUNT(id)"); + + LOG.debug("Query: {}, parameters: {}", query, parameters); + + return neo4jTemplate.count(query.toString(), parameters); + } + + protected List parseOrderBy(final Streamable orderBy) { + List clauses = new ArrayList<>(); + + Set orderByUniquePlainSchemas = new HashSet<>(); + Set orderByNonUniquePlainSchemas = new HashSet<>(); + orderBy.forEach(clause -> { + if (realmUtils.getField(clause.getProperty()).isPresent()) { + clauses.add(clause.getProperty() + " " + clause.getDirection().name()); + } else { + plainSchemaDAO.findById(clause.getProperty()).ifPresent(schema -> { + if (schema.isUniqueConstraint()) { + orderByUniquePlainSchemas.add(schema.getKey()); + } else { + orderByNonUniquePlainSchemas.add(schema.getKey()); + } + if (orderByUniquePlainSchemas.size() > 1 || orderByNonUniquePlainSchemas.size() > 1) { + SyncopeClientException invalidSearch = + SyncopeClientException.build(ClientExceptionType.InvalidSearchParameters); + invalidSearch.getElements().add("Order by more than one attribute is not allowed; " + + "remove one from " + (orderByUniquePlainSchemas.size() > 1 + ? orderByUniquePlainSchemas : orderByNonUniquePlainSchemas)); + throw invalidSearch; + } + + clauses.add(schema.getKey() + " " + clause.getDirection().name()); + }); + } + }); + + return clauses; } @Override - public List findDescendants(final String base, final String prefix) { + protected List doSearch(final Set bases, final SearchCond cond, final Pageable pageable) { Map parameters = new HashMap<>(); - StringBuilder queryString = buildDescendantsQuery(Set.of(base), null, parameters). - append(" AND (n.fullPath = $prefix OR n.fullPath =~ $likePrefix)"). - append(" RETURN n.id ORDER BY n.fullPath"); - parameters.put("prefix", prefix); - parameters.put("likePrefix", SyncopeConstants.ROOT_REALM.equals(prefix) ? "/.*" : prefix + "/.*"); + QueryInfo queryInfo = getQuery(cond, parameters); - return neo4jClient.query(queryString.toString()). - bindAll(parameters).fetch().all().stream(). - map(found -> (String) found.values().iterator().next()).toList(); + wrapQuery(bases, queryInfo, pageable.getSort(), parameters); + TextStringBuilder query = queryInfo.query(); + + List orderBy = parseOrderBy(pageable.getSort()); + String orderByStmt = String.join(", ", orderBy); + + query.append("RETURN id "). + append("ORDER BY ").append(orderByStmt); + + if (pageable.isPaged()) { + query.append(" SKIP ").append(pageable.getPageSize() * pageable.getPageNumber()). + append(" LIMIT ").append(pageable.getPageSize()); + } + + LOG.debug("Query: {}, parameters: {}", query, parameters); + + return toList(neo4jClient.query( + query.toString()).bindAll(parameters).fetch().all(), "id"); } } diff --git a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jTaskDAO.java b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jTaskDAO.java index 0251ab76c2b..0bbea61c08c 100644 --- a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jTaskDAO.java +++ b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jTaskDAO.java @@ -320,7 +320,7 @@ protected StringBuilder query( Stream realmKeys = AuthContextUtils.getAuthorizations().get(IdRepoEntitlement.TASK_LIST).stream(). map(realmSearchDAO::findByFullPath). filter(Optional::isPresent). - flatMap(r -> realmSearchDAO.findDescendants(r.get().getFullPath(), null, Pageable.unpaged()). + flatMap(r -> realmSearchDAO.findDescendants(r.get().getFullPath(), null). stream()). map(Realm::getKey). distinct(); diff --git a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/repo/AbstractAnyRepoExt.java b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/repo/AbstractAnyRepoExt.java index fe61c79e94e..a0cdb9f082b 100644 --- a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/repo/AbstractAnyRepoExt.java +++ b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/repo/AbstractAnyRepoExt.java @@ -49,7 +49,6 @@ 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.user.User; -import org.apache.syncope.core.persistence.common.dao.AnyFinder; import org.apache.syncope.core.persistence.neo4j.dao.AbstractDAO; import org.apache.syncope.core.persistence.neo4j.entity.AbstractAny; import org.apache.syncope.core.persistence.neo4j.entity.EntityCacheKey; @@ -97,8 +96,6 @@ protected static List split(final String attrValue, final List l protected final DynRealmDAO dynRealmDAO; - protected final AnyFinder anyFinder; - protected final AnyUtils anyUtils; protected AbstractAnyRepoExt( @@ -107,7 +104,6 @@ protected AbstractAnyRepoExt( final PlainSchemaDAO plainSchemaDAO, final DerSchemaDAO derSchemaDAO, final DynRealmDAO dynRealmDAO, - final AnyFinder anyFinder, final AnyUtils anyUtils, final Neo4jTemplate neo4jTemplate, final Neo4jClient neo4jClient) { @@ -118,7 +114,6 @@ protected AbstractAnyRepoExt( this.plainSchemaDAO = plainSchemaDAO; this.derSchemaDAO = derSchemaDAO; this.dynRealmDAO = dynRealmDAO; - this.anyFinder = anyFinder; this.anyUtils = anyUtils; } @@ -165,11 +160,6 @@ public A authFind(final String key) { return any; } - @Override - public List findByDerAttrValue(final String expression, final String value, final boolean ignoreCaseMatch) { - return anyFinder.findByDerAttrValue(anyUtils.anyTypeKind(), expression, value, ignoreCaseMatch); - } - @Transactional(propagation = Propagation.REQUIRES_NEW, readOnly = true) @Override @SuppressWarnings("unchecked") diff --git a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/repo/AnyObjectRepoExtImpl.java b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/repo/AnyObjectRepoExtImpl.java index 158934b86bb..6e7d9f0335e 100644 --- a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/repo/AnyObjectRepoExtImpl.java +++ b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/repo/AnyObjectRepoExtImpl.java @@ -50,7 +50,6 @@ import org.apache.syncope.core.persistence.api.entity.group.Group; import org.apache.syncope.core.persistence.api.entity.user.URelationship; import org.apache.syncope.core.persistence.api.utils.RealmUtils; -import org.apache.syncope.core.persistence.common.dao.AnyFinder; import org.apache.syncope.core.persistence.neo4j.entity.EntityCacheKey; import org.apache.syncope.core.persistence.neo4j.entity.Neo4jAnyType; import org.apache.syncope.core.persistence.neo4j.entity.Neo4jAnyTypeClass; @@ -88,7 +87,6 @@ public AnyObjectRepoExtImpl( final DynRealmDAO dynRealmDAO, final UserDAO userDAO, final GroupDAO groupDAO, - final AnyFinder anyFinder, final Neo4jTemplate neo4jTemplate, final Neo4jClient neo4jClient, final NodeValidator nodeValidator, @@ -100,7 +98,6 @@ public AnyObjectRepoExtImpl( plainSchemaDAO, derSchemaDAO, dynRealmDAO, - anyFinder, anyUtilsFactory.getInstance(AnyTypeKind.ANY_OBJECT), neo4jTemplate, neo4jClient); diff --git a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/repo/AnyRepoExt.java b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/repo/AnyRepoExt.java index a2306a8ce64..158ca0922d6 100644 --- a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/repo/AnyRepoExt.java +++ b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/repo/AnyRepoExt.java @@ -75,8 +75,6 @@ static String membNode(final AnyTypeKind anyTypeKind) { A authFind(String key); - List findByDerAttrValue(String expression, String value, boolean ignoreCaseMatch); - AllowedSchemas findAllowedSchemas(A any, Class reference); List findDynRealms(String key); diff --git a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/repo/DynRealmRepoExtImpl.java b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/repo/DynRealmRepoExtImpl.java index 105f7e76095..d32d7b73bf0 100644 --- a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/repo/DynRealmRepoExtImpl.java +++ b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/repo/DynRealmRepoExtImpl.java @@ -29,8 +29,8 @@ import org.apache.syncope.core.persistence.api.dao.UserDAO; import org.apache.syncope.core.persistence.api.entity.Any; import org.apache.syncope.core.persistence.api.entity.DynRealm; +import org.apache.syncope.core.persistence.api.search.AnySearchCondVisitor; import org.apache.syncope.core.persistence.api.search.SearchCondConverter; -import org.apache.syncope.core.persistence.api.search.SearchCondVisitor; import org.apache.syncope.core.persistence.neo4j.dao.AbstractDAO; import org.apache.syncope.core.persistence.neo4j.entity.Neo4jDynRealm; import org.apache.syncope.core.persistence.neo4j.entity.Neo4jDynRealmMembership; @@ -60,7 +60,7 @@ public class DynRealmRepoExtImpl extends AbstractDAO implements DynRealmRepoExt protected final AnyMatchDAO anyMatchDAO; - protected final SearchCondVisitor searchCondVisitor; + protected final AnySearchCondVisitor searchCondVisitor; protected final NodeValidator nodeValidator; @@ -71,7 +71,7 @@ public DynRealmRepoExtImpl( final AnyObjectDAO anyObjectDAO, final AnySearchDAO searchDAO, final AnyMatchDAO anyMatchDAO, - final SearchCondVisitor searchCondVisitor, + final AnySearchCondVisitor searchCondVisitor, final Neo4jTemplate neo4jTemplate, final Neo4jClient neo4jClient, final NodeValidator nodeValidator) { diff --git a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/repo/GroupRepoExtImpl.java b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/repo/GroupRepoExtImpl.java index 52e4cc28508..20fab129fa5 100644 --- a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/repo/GroupRepoExtImpl.java +++ b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/repo/GroupRepoExtImpl.java @@ -58,10 +58,9 @@ import org.apache.syncope.core.persistence.api.entity.user.UDynGroupMembership; import org.apache.syncope.core.persistence.api.entity.user.UMembership; import org.apache.syncope.core.persistence.api.entity.user.User; +import org.apache.syncope.core.persistence.api.search.AnySearchCondVisitor; import org.apache.syncope.core.persistence.api.search.SearchCondConverter; -import org.apache.syncope.core.persistence.api.search.SearchCondVisitor; import org.apache.syncope.core.persistence.api.utils.RealmUtils; -import org.apache.syncope.core.persistence.common.dao.AnyFinder; import org.apache.syncope.core.persistence.neo4j.entity.EntityCacheKey; import org.apache.syncope.core.persistence.neo4j.entity.Neo4jAnyType; import org.apache.syncope.core.persistence.neo4j.entity.Neo4jAnyTypeClass; @@ -101,7 +100,7 @@ public class GroupRepoExtImpl extends AbstractAnyRepoExt impl protected final AnySearchDAO anySearchDAO; - protected final SearchCondVisitor searchCondVisitor; + protected final AnySearchCondVisitor searchCondVisitor; protected final NodeValidator nodeValidator; @@ -120,8 +119,7 @@ public GroupRepoExtImpl( final UserDAO userDAO, final AnyObjectDAO anyObjectDAO, final AnySearchDAO anySearchDAO, - final AnyFinder anyFinder, - final SearchCondVisitor searchCondVisitor, + final AnySearchCondVisitor searchCondVisitor, final Neo4jTemplate neo4jTemplate, final Neo4jClient neo4jClient, final NodeValidator nodeValidator, @@ -133,7 +131,6 @@ public GroupRepoExtImpl( plainSchemaDAO, derSchemaDAO, dynRealmDAO, - anyFinder, anyUtilsFactory.getInstance(AnyTypeKind.GROUP), neo4jTemplate, neo4jClient); diff --git a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/repo/RoleRepoExtImpl.java b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/repo/RoleRepoExtImpl.java index 35608fc0134..9fa5d04eec0 100644 --- a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/repo/RoleRepoExtImpl.java +++ b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/repo/RoleRepoExtImpl.java @@ -29,8 +29,8 @@ import org.apache.syncope.core.persistence.api.entity.Realm; import org.apache.syncope.core.persistence.api.entity.Role; import org.apache.syncope.core.persistence.api.entity.user.User; +import org.apache.syncope.core.persistence.api.search.AnySearchCondVisitor; import org.apache.syncope.core.persistence.api.search.SearchCondConverter; -import org.apache.syncope.core.persistence.api.search.SearchCondVisitor; import org.apache.syncope.core.persistence.neo4j.dao.AbstractDAO; import org.apache.syncope.core.persistence.neo4j.entity.EntityCacheKey; import org.apache.syncope.core.persistence.neo4j.entity.Neo4jRealm; @@ -55,7 +55,7 @@ public class RoleRepoExtImpl extends AbstractDAO implements RoleRepoExt { protected final DelegationDAO delegationDAO; - protected final SearchCondVisitor searchCondVisitor; + protected final AnySearchCondVisitor searchCondVisitor; protected final NodeValidator nodeValidator; @@ -66,7 +66,7 @@ public RoleRepoExtImpl( final AnyMatchDAO anyMatchDAO, final AnySearchDAO anySearchDAO, final DelegationDAO delegationDAO, - final SearchCondVisitor searchCondVisitor, + final AnySearchCondVisitor searchCondVisitor, final Neo4jTemplate neo4jTemplate, final Neo4jClient neo4jClient, final NodeValidator nodeValidator, diff --git a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/repo/UserRepoExtImpl.java b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/repo/UserRepoExtImpl.java index fd5944b0033..37f756fc094 100644 --- a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/repo/UserRepoExtImpl.java +++ b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/repo/UserRepoExtImpl.java @@ -52,7 +52,6 @@ import org.apache.syncope.core.persistence.api.entity.user.URelationship; import org.apache.syncope.core.persistence.api.entity.user.User; import org.apache.syncope.core.persistence.api.utils.RealmUtils; -import org.apache.syncope.core.persistence.common.dao.AnyFinder; import org.apache.syncope.core.persistence.neo4j.entity.EntityCacheKey; import org.apache.syncope.core.persistence.neo4j.entity.Neo4jAnyTypeClass; import org.apache.syncope.core.persistence.neo4j.entity.Neo4jExternalResource; @@ -103,7 +102,6 @@ public UserRepoExtImpl( final GroupDAO groupDAO, final DelegationDAO delegationDAO, final FIQLQueryDAO fiqlQueryDAO, - final AnyFinder anyFinder, final SecurityProperties securityProperties, final Neo4jTemplate neo4jTemplate, final Neo4jClient neo4jClient, @@ -116,7 +114,6 @@ public UserRepoExtImpl( plainSchemaDAO, derSchemaDAO, dynRealmDAO, - anyFinder, anyUtilsFactory.getInstance(AnyTypeKind.USER), neo4jTemplate, neo4jClient); diff --git a/core/persistence-neo4j/src/test/java/org/apache/syncope/core/persistence/neo4j/inner/AnySearchTest.java b/core/persistence-neo4j/src/test/java/org/apache/syncope/core/persistence/neo4j/inner/AnySearchTest.java index 4fdf17e0e2d..3f3f1b63c6f 100644 --- a/core/persistence-neo4j/src/test/java/org/apache/syncope/core/persistence/neo4j/inner/AnySearchTest.java +++ b/core/persistence-neo4j/src/test/java/org/apache/syncope/core/persistence/neo4j/inner/AnySearchTest.java @@ -41,6 +41,7 @@ import org.apache.syncope.core.persistence.api.dao.AnyObjectDAO; import org.apache.syncope.core.persistence.api.dao.AnySearchDAO; import org.apache.syncope.core.persistence.api.dao.AnyTypeDAO; +import org.apache.syncope.core.persistence.api.dao.DerSchemaDAO; import org.apache.syncope.core.persistence.api.dao.GroupDAO; import org.apache.syncope.core.persistence.api.dao.PlainSchemaDAO; import org.apache.syncope.core.persistence.api.dao.RealmDAO; @@ -113,6 +114,9 @@ public class AnySearchTest extends AbstractTest { @Autowired private PlainSchemaDAO plainSchemaDAO; + @Autowired + private DerSchemaDAO derSchemaDAO; + @Autowired private PlainAttrValidationManager validator; @@ -750,13 +754,13 @@ public void asGroupOwner() { assertEquals( 1, searchDAO.count( - realmDAO.getRoot(), true, authRealms, groupDAO.getAllMatchingCond(), AnyTypeKind.GROUP)); + realmDAO.getRoot(), true, authRealms, searchDAO.getAllMatchingCond(), AnyTypeKind.GROUP)); List groups = searchDAO.search( realmDAO.getRoot(), true, authRealms, - groupDAO.getAllMatchingCond(), + searchDAO.getAllMatchingCond(), PageRequest.of(0, 10), AnyTypeKind.GROUP); assertEquals(1, groups.size()); @@ -783,6 +787,35 @@ public void changePwdDate() { assertEquals(5, users.size()); } + @Test + public void findByDerAttrValue() { + List list = searchDAO.findByDerAttrValue( + derSchemaDAO.findById("cn").orElseThrow().getExpression(), "Vivaldi, Antonio", false, AnyTypeKind.USER); + assertEquals(1, list.size()); + + list = searchDAO.findByDerAttrValue( + derSchemaDAO.findById("cn").orElseThrow().getExpression(), "VIVALDI, ANTONIO", false, AnyTypeKind.USER); + assertEquals(0, list.size()); + + list = searchDAO.findByDerAttrValue( + derSchemaDAO.findById("cn").orElseThrow().getExpression(), "VIVALDI, ANTONIO", true, AnyTypeKind.USER); + assertEquals(1, list.size()); + } + + @Test + public void findByInvalidDerAttrValue() { + assertTrue(searchDAO.findByDerAttrValue( + derSchemaDAO.findById("cn").orElseThrow().getExpression(), + "Antonio, Maria, Rossi", false, AnyTypeKind.USER).isEmpty()); + } + + @Test + public void findByInvalidDerAttrExpression() { + assertThrows(IllegalArgumentException.class, () -> searchDAO.findByDerAttrValue( + derSchemaDAO.findById("noschema").orElseThrow().getExpression(), + "Antonio, Maria", false, AnyTypeKind.USER).isEmpty()); + } + @Test public void issue202() { ResourceCond ws2 = new ResourceCond(); diff --git a/core/persistence-neo4j/src/test/java/org/apache/syncope/core/persistence/neo4j/inner/MultitenancyTest.java b/core/persistence-neo4j/src/test/java/org/apache/syncope/core/persistence/neo4j/inner/MultitenancyTest.java index 594124a0ad2..dfc7ee33e1c 100644 --- a/core/persistence-neo4j/src/test/java/org/apache/syncope/core/persistence/neo4j/inner/MultitenancyTest.java +++ b/core/persistence-neo4j/src/test/java/org/apache/syncope/core/persistence/neo4j/inner/MultitenancyTest.java @@ -34,7 +34,6 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.domain.Pageable; import org.springframework.transaction.annotation.Transactional; @Transactional @@ -70,10 +69,10 @@ public void readPlainSchemas() { public void readRealm() { assertEquals( 1, - realmSearchDAO.findDescendants(realmDAO.getRoot().getFullPath(), null, Pageable.unpaged()).size()); + realmSearchDAO.findDescendants(realmDAO.getRoot().getFullPath(), null).size()); assertEquals( realmDAO.getRoot(), - realmSearchDAO.findDescendants(realmDAO.getRoot().getFullPath(), null, Pageable.unpaged()).getFirst()); + realmSearchDAO.findDescendants(realmDAO.getRoot().getFullPath(), null).getFirst()); } @Test diff --git a/core/persistence-neo4j/src/test/java/org/apache/syncope/core/persistence/neo4j/inner/RealmTest.java b/core/persistence-neo4j/src/test/java/org/apache/syncope/core/persistence/neo4j/inner/RealmTest.java index 578cb3f8918..ad53b741b5f 100644 --- a/core/persistence-neo4j/src/test/java/org/apache/syncope/core/persistence/neo4j/inner/RealmTest.java +++ b/core/persistence-neo4j/src/test/java/org/apache/syncope/core/persistence/neo4j/inner/RealmTest.java @@ -86,14 +86,11 @@ public void findInvalidPath() { @Test public void findDescendants() { - List found = realmSearchDAO.findDescendants(SyncopeConstants.ROOT_REALM, SyncopeConstants.ROOT_REALM); + List found = realmSearchDAO.findDescendants(SyncopeConstants.ROOT_REALM, SyncopeConstants.ROOT_REALM); assertEquals(4, found.size()); - assertTrue(found.stream().allMatch(f -> SyncopeConstants.UUID_PATTERN.matcher(f).matches())); + assertTrue(found.stream().allMatch(f -> SyncopeConstants.UUID_PATTERN.matcher(f.getKey()).matches())); - assertEquals( - found, - realmSearchDAO.findDescendants(SyncopeConstants.ROOT_REALM, null, Pageable.unpaged()).stream(). - map(Realm::getKey).toList()); + assertEquals(found, realmSearchDAO.findDescendants(SyncopeConstants.ROOT_REALM, null)); } @Test @@ -110,14 +107,14 @@ public void findChildren() { @Test public void findAll() { - List list = realmSearchDAO.findDescendants(realmDAO.getRoot().getFullPath(), null, Pageable.unpaged()); + List list = realmSearchDAO.findDescendants(realmDAO.getRoot().getFullPath(), null); assertNotNull(list); assertFalse(list.isEmpty()); list.forEach(Assertions::assertNotNull); assertEquals(4, realmDAO.findAll(Pageable.ofSize(100)).stream().count()); - list = realmSearchDAO.findDescendants(Set.of("/even", "/odd"), null, Pageable.unpaged()); + list = realmSearchDAO.search(Set.of("/even", "/odd"), realmSearchDAO.getAllMatchingCond(), Pageable.unpaged()); assertEquals(3, list.size()); assertNotNull(list.stream().filter(realm -> "even".equals(realm.getName())).findFirst().orElseThrow()); assertNotNull(list.stream().filter(realm -> "two".equals(realm.getName())).findFirst().orElseThrow()); diff --git a/core/persistence-neo4j/src/test/java/org/apache/syncope/core/persistence/neo4j/inner/UserTest.java b/core/persistence-neo4j/src/test/java/org/apache/syncope/core/persistence/neo4j/inner/UserTest.java index af058ee7b2d..bea5c20d273 100644 --- a/core/persistence-neo4j/src/test/java/org/apache/syncope/core/persistence/neo4j/inner/UserTest.java +++ b/core/persistence-neo4j/src/test/java/org/apache/syncope/core/persistence/neo4j/inner/UserTest.java @@ -30,7 +30,6 @@ import java.util.List; import org.apache.syncope.common.lib.types.CipherAlgorithm; import org.apache.syncope.core.persistence.api.EncryptorManager; -import org.apache.syncope.core.persistence.api.dao.DerSchemaDAO; import org.apache.syncope.core.persistence.api.dao.ExternalResourceDAO; import org.apache.syncope.core.persistence.api.dao.RealmDAO; import org.apache.syncope.core.persistence.api.dao.RealmSearchDAO; @@ -62,9 +61,6 @@ public class UserTest extends AbstractTest { @Autowired private ExternalResourceDAO resourceDAO; - @Autowired - private DerSchemaDAO derSchemaDAO; - @Autowired private SecurityQuestionDAO securityQuestionDAO; @@ -110,33 +106,6 @@ public void count() { assertEquals(5, count); } - @Test - public void findByDerAttrValue() { - List list = userDAO.findByDerAttrValue( - derSchemaDAO.findById("cn").orElseThrow().getExpression(), "Vivaldi, Antonio", false); - assertEquals(1, list.size()); - - list = userDAO.findByDerAttrValue( - derSchemaDAO.findById("cn").orElseThrow().getExpression(), "VIVALDI, ANTONIO", false); - assertEquals(0, list.size()); - - list = userDAO.findByDerAttrValue( - derSchemaDAO.findById("cn").orElseThrow().getExpression(), "VIVALDI, ANTONIO", true); - assertEquals(1, list.size()); - } - - @Test - public void findByInvalidDerAttrValue() { - assertTrue(userDAO.findByDerAttrValue( - derSchemaDAO.findById("cn").orElseThrow().getExpression(), "Antonio, Maria, Rossi", false).isEmpty()); - } - - @Test - public void findByInvalidDerAttrExpression() { - assertThrows(IllegalArgumentException.class, () -> userDAO.findByDerAttrValue( - derSchemaDAO.findById("noschema").orElseThrow().getExpression(), "Antonio, Maria", false).isEmpty()); - } - @Test public void findByKey() { assertTrue(userDAO.findById("1417acbe-cbf6-4277-9372-e75e04f97000").isPresent()); diff --git a/core/persistence-neo4j/src/test/java/org/apache/syncope/core/persistence/neo4j/outer/AnySearchTest.java b/core/persistence-neo4j/src/test/java/org/apache/syncope/core/persistence/neo4j/outer/AnySearchTest.java index 3a829ede144..f94ee1c5db1 100644 --- a/core/persistence-neo4j/src/test/java/org/apache/syncope/core/persistence/neo4j/outer/AnySearchTest.java +++ b/core/persistence-neo4j/src/test/java/org/apache/syncope/core/persistence/neo4j/outer/AnySearchTest.java @@ -34,6 +34,7 @@ import org.apache.syncope.core.persistence.api.attrvalue.PlainAttrValidationManager; import org.apache.syncope.core.persistence.api.dao.AnyObjectDAO; import org.apache.syncope.core.persistence.api.dao.AnySearchDAO; +import org.apache.syncope.core.persistence.api.dao.DerSchemaDAO; import org.apache.syncope.core.persistence.api.dao.GroupDAO; import org.apache.syncope.core.persistence.api.dao.RealmDAO; import org.apache.syncope.core.persistence.api.dao.RealmSearchDAO; @@ -44,6 +45,7 @@ import org.apache.syncope.core.persistence.api.dao.search.AttrCond; 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.DerSchema; import org.apache.syncope.core.persistence.api.entity.PlainAttr; import org.apache.syncope.core.persistence.api.entity.Role; import org.apache.syncope.core.persistence.api.entity.anyobject.AMembership; @@ -83,6 +85,9 @@ public class AnySearchTest extends AbstractTest { @Autowired private RoleDAO roleDAO; + @Autowired + private DerSchemaDAO derSchemaDAO; + @Autowired private PlainAttrValidationManager validator; @@ -212,6 +217,41 @@ public void issueSYNCOPE95() { assertEquals("c9b2dec2-00a7-4855-97c0-d854842b4b24", users.getFirst().getKey()); } + @Test + public void issueSYNCOPE800() { + // create derived attribute (literal as prefix) + DerSchema prefix = entityFactory.newEntity(DerSchema.class); + prefix.setKey("kprefix"); + prefix.setExpression("'k' + firstname"); + + derSchemaDAO.save(prefix); + + // create derived attribute (literal as suffix) + DerSchema suffix = entityFactory.newEntity(DerSchema.class); + suffix.setKey("ksuffix"); + suffix.setExpression("firstname + 'k'"); + + derSchemaDAO.save(suffix); + + // add derived attributes to user + User owner = userDAO.findByUsername("vivaldi").orElseThrow(); + + String firstname = owner.getPlainAttr("firstname").get().getValuesAsStrings().getFirst(); + assertNotNull(firstname); + + // search by ksuffix derived attribute + List list = searchDAO.findByDerAttrValue( + derSchemaDAO.findById("ksuffix").orElseThrow().getExpression(), + firstname + 'k', false, AnyTypeKind.USER); + assertEquals(1, list.size()); + + // search by kprefix derived attribute + list = searchDAO.findByDerAttrValue( + derSchemaDAO.findById("kprefix").orElseThrow().getExpression(), + 'k' + firstname, false, AnyTypeKind.USER); + assertEquals(1, list.size()); + } + @Test public void issueSYNCOPE1417() { AnyCond usernameLeafCond = new AnyCond(AnyCond.Type.EQ); diff --git a/core/persistence-neo4j/src/test/java/org/apache/syncope/core/persistence/neo4j/outer/RealmTest.java b/core/persistence-neo4j/src/test/java/org/apache/syncope/core/persistence/neo4j/outer/RealmTest.java index 941889f3cbb..cd457b900b4 100644 --- a/core/persistence-neo4j/src/test/java/org/apache/syncope/core/persistence/neo4j/outer/RealmTest.java +++ b/core/persistence-neo4j/src/test/java/org/apache/syncope/core/persistence/neo4j/outer/RealmTest.java @@ -18,10 +18,15 @@ */ package org.apache.syncope.core.persistence.neo4j.outer; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.util.List; +import java.util.Set; import java.util.UUID; +import org.apache.syncope.common.lib.SyncopeConstants; import org.apache.syncope.common.lib.types.IdRepoImplementationType; import org.apache.syncope.common.lib.types.ImplementationEngine; import org.apache.syncope.core.persistence.api.attrvalue.PlainAttrValidationManager; @@ -31,6 +36,9 @@ import org.apache.syncope.core.persistence.api.dao.RealmDAO; import org.apache.syncope.core.persistence.api.dao.RealmSearchDAO; import org.apache.syncope.core.persistence.api.dao.RoleDAO; +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.Implementation; import org.apache.syncope.core.persistence.api.entity.PlainAttr; import org.apache.syncope.core.persistence.api.entity.Realm; @@ -39,6 +47,8 @@ import org.apache.syncope.core.persistence.neo4j.AbstractTest; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; import org.springframework.transaction.annotation.Transactional; @Transactional @@ -136,4 +146,120 @@ public void addAndRemoveLogicActions() { realm = realmDAO.findById("722f3d84-9c2b-4525-8f6e-e4b82c55a36c").orElseThrow(); assertTrue(realm.getActions().isEmpty()); } + + @Test + public void search() { + Realm two = realmSearchDAO.findByFullPath("/even/two").orElseThrow(); + two.add(anyTypeClassDAO.findById("other").orElseThrow()); + two = realmDAO.save(two); + + two = realmDAO.findById(two.getKey()).orElseThrow(); + PlainAttr aLong = new PlainAttr(); + aLong.setSchema("aLong"); + aLong.add(validator, "42"); + two.add(aLong); + two = realmDAO.save(two); + + Realm odd = realmSearchDAO.findByFullPath("/odd").orElseThrow(); + odd.add(anyTypeClassDAO.findById("other").orElseThrow()); + odd = realmDAO.save(odd); + + odd = realmDAO.findById(odd.getKey()).orElseThrow(); + PlainAttr oddLong = new PlainAttr(); + oddLong.setSchema("aLong"); + oddLong.add(validator, "99"); + odd.add(oddLong); + realmDAO.save(odd); + + two = realmDAO.findById(two.getKey()).orElseThrow(); + assertEquals(anyTypeClassDAO.findById("other").orElseThrow(), two.getAnyTypeClasses().iterator().next()); + assertEquals(1, two.getPlainAttrs().size()); + assertEquals(42, two.getPlainAttr("aLong").orElseThrow().getValues().getFirst().getLongValue()); + + odd = realmDAO.findById(odd.getKey()).orElseThrow(); + assertEquals(anyTypeClassDAO.findById("other").orElseThrow(), odd.getAnyTypeClasses().iterator().next()); + assertEquals(1, odd.getPlainAttrs().size()); + assertEquals(99, odd.getPlainAttr("aLong").orElseThrow().getValues().getFirst().getLongValue()); + + AnyCond name = new AnyCond(AttrCond.Type.EQ); + name.setSchema("name"); + name.setExpression("two"); + + List result = realmSearchDAO.search( + Set.of(SyncopeConstants.ROOT_REALM), + SearchCond.of(name), + Pageable.unpaged()); + assertEquals(1, result.size()); + assertEquals("two", result.getFirst().getName()); + + AttrCond attrEq = new AttrCond(AttrCond.Type.EQ); + attrEq.setSchema("aLong"); + attrEq.setExpression("42"); + + result = realmSearchDAO.search( + Set.of(SyncopeConstants.ROOT_REALM), + SearchCond.of(attrEq), + Pageable.unpaged()); + assertEquals(1, result.size()); + assertEquals("two", result.getFirst().getName()); + + result = realmSearchDAO.search( + Set.of(SyncopeConstants.ROOT_REALM), + SearchCond.and(SearchCond.of(name), SearchCond.of(attrEq)), + Pageable.unpaged()); + assertEquals(1, result.size()); + assertEquals("two", result.getFirst().getName()); + + AttrCond attrEq2 = new AttrCond(AttrCond.Type.EQ); + attrEq2.setSchema("aLong"); + attrEq2.setExpression("99"); + + result = realmSearchDAO.search( + Set.of(SyncopeConstants.ROOT_REALM), + SearchCond.or(SearchCond.of(attrEq), SearchCond.of(attrEq2)), + Pageable.unpaged()); + assertTrue(result.stream().anyMatch(r -> "two".equals(r.getName()))); + assertTrue(result.stream().anyMatch(r -> "odd".equals(r.getName()))); + + AttrCond attrIsNull = new AttrCond(AttrCond.Type.ISNULL); + attrIsNull.setSchema("aLong"); + + result = realmSearchDAO.search( + Set.of(SyncopeConstants.ROOT_REALM), + SearchCond.of(attrIsNull), + Pageable.unpaged()); + result.forEach(r -> assertFalse("two".equals(r.getName()) || "odd".equals(r.getName()))); + + AttrCond attrIsNotNull = new AttrCond(AttrCond.Type.ISNOTNULL); + attrIsNotNull.setSchema("aLong"); + + assertEquals(2, realmSearchDAO.count( + Set.of(SyncopeConstants.ROOT_REALM), + SearchCond.of(attrIsNotNull))); + + result = realmSearchDAO.search( + Set.of(SyncopeConstants.ROOT_REALM), + SearchCond.of(attrIsNotNull), + Pageable.unpaged(Sort.by(new Sort.Order(Sort.Direction.ASC, "name")))); + assertEquals("odd", result.get(0).getName()); + assertEquals("two", result.get(1).getName()); + + result = realmSearchDAO.search( + Set.of(SyncopeConstants.ROOT_REALM), + SearchCond.of(attrIsNotNull), + Pageable.unpaged(Sort.by(new Sort.Order(Sort.Direction.ASC, "aLong")))); + assertEquals("two", result.get(0).getName()); + assertEquals("odd", result.get(1).getName()); + + AttrCond cond1 = new AttrCond(AttrCond.Type.EQ); + cond1.setSchema("aLong"); + cond1.setExpression("42"); + AttrCond cond2 = new AttrCond(AttrCond.Type.IEQ); + cond2.setSchema("ctype"); + cond2.setExpression("string"); + assertDoesNotThrow(() -> realmSearchDAO.search( + Set.of(SyncopeConstants.ROOT_REALM), + SearchCond.and(SearchCond.of(cond1), SearchCond.of(cond2)), + Pageable.unpaged())); + } } diff --git a/core/persistence-neo4j/src/test/java/org/apache/syncope/core/persistence/neo4j/outer/UserTest.java b/core/persistence-neo4j/src/test/java/org/apache/syncope/core/persistence/neo4j/outer/UserTest.java index 9f596eb4025..5f8a7c275d4 100644 --- a/core/persistence-neo4j/src/test/java/org/apache/syncope/core/persistence/neo4j/outer/UserTest.java +++ b/core/persistence-neo4j/src/test/java/org/apache/syncope/core/persistence/neo4j/outer/UserTest.java @@ -30,7 +30,6 @@ import org.apache.syncope.core.persistence.api.attrvalue.PlainAttrValidationManager; import org.apache.syncope.core.persistence.api.dao.AnyObjectDAO; import org.apache.syncope.core.persistence.api.dao.DelegationDAO; -import org.apache.syncope.core.persistence.api.dao.DerSchemaDAO; import org.apache.syncope.core.persistence.api.dao.ExternalResourceDAO; import org.apache.syncope.core.persistence.api.dao.GroupDAO; import org.apache.syncope.core.persistence.api.dao.PlainSchemaDAO; @@ -38,7 +37,6 @@ import org.apache.syncope.core.persistence.api.dao.RoleDAO; import org.apache.syncope.core.persistence.api.dao.UserDAO; import org.apache.syncope.core.persistence.api.entity.Delegation; -import org.apache.syncope.core.persistence.api.entity.DerSchema; import org.apache.syncope.core.persistence.api.entity.PlainAttr; import org.apache.syncope.core.persistence.api.entity.Role; import org.apache.syncope.core.persistence.api.entity.user.LinkedAccount; @@ -71,9 +69,6 @@ public class UserTest extends AbstractTest { @Autowired private PlainSchemaDAO plainSchemaDAO; - @Autowired - private DerSchemaDAO derSchemaDAO; - @Autowired private ExternalResourceDAO resourceDAO; @@ -224,40 +219,4 @@ public void deleteCascadeOnDelegations() { assertTrue(delegationDAO.findById(delegation.getKey()).isEmpty()); } - - /** - * Search by derived attribute. - */ - @Test - public void issueSYNCOPE800() { - // create derived attribute (literal as prefix) - DerSchema prefix = entityFactory.newEntity(DerSchema.class); - prefix.setKey("kprefix"); - prefix.setExpression("'k' + firstname"); - - derSchemaDAO.save(prefix); - - // create derived attribute (literal as suffix) - DerSchema suffix = entityFactory.newEntity(DerSchema.class); - suffix.setKey("ksuffix"); - suffix.setExpression("firstname + 'k'"); - - derSchemaDAO.save(suffix); - - // add derived attributes to user - User owner = userDAO.findByUsername("vivaldi").orElseThrow(); - - String firstname = owner.getPlainAttr("firstname").get().getValuesAsStrings().getFirst(); - assertNotNull(firstname); - - // search by ksuffix derived attribute - List list = userDAO.findByDerAttrValue( - derSchemaDAO.findById("ksuffix").orElseThrow().getExpression(), firstname + 'k', false); - assertEquals(1, list.size()); - - // search by kprefix derived attribute - list = userDAO.findByDerAttrValue( - derSchemaDAO.findById("kprefix").orElseThrow().getExpression(), 'k' + firstname, false); - assertEquals(1, list.size()); - } } diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/ProvisioningContext.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/ProvisioningContext.java index b20f4a6856a..98e90f125ba 100644 --- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/ProvisioningContext.java +++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/ProvisioningContext.java @@ -64,7 +64,7 @@ import org.apache.syncope.core.persistence.api.entity.AnyUtilsFactory; import org.apache.syncope.core.persistence.api.entity.EntityFactory; import org.apache.syncope.core.persistence.api.entity.task.TaskUtilsFactory; -import org.apache.syncope.core.persistence.api.search.SearchCondVisitor; +import org.apache.syncope.core.persistence.api.search.AnySearchCondVisitor; import org.apache.syncope.core.persistence.api.utils.RealmUtils; import org.apache.syncope.core.provisioning.api.AnyObjectProvisioningManager; import org.apache.syncope.core.provisioning.api.AuditEventProcessor; @@ -576,7 +576,7 @@ public AnyObjectProvisioningManager anyObjectProvisioningManager( @Bean public NotificationManager notificationManager( final EntityFactory entityFactory, - final SearchCondVisitor searchCondVisitor, + final AnySearchCondVisitor searchCondVisitor, final DerSchemaDAO derSchemaDAO, final NotificationDAO notificationDAO, final AnyObjectDAO anyObjectDAO, @@ -841,7 +841,7 @@ public DelegationDataBinder delegationDataBinder( @ConditionalOnMissingBean @Bean public FIQLQueryDataBinder fiqlQueryDataBinder( - final SearchCondVisitor searchCondVisitor, + final AnySearchCondVisitor searchCondVisitor, final UserDAO userDAO, final EntityFactory entityFactory) { @@ -853,7 +853,7 @@ public FIQLQueryDataBinder fiqlQueryDataBinder( public DynRealmDataBinder dynRealmDataBinder( final AnyTypeDAO anyTypeDAO, final DynRealmDAO dynRealmDAO, - final SearchCondVisitor searchCondVisitor, + final AnySearchCondVisitor searchCondVisitor, final EntityFactory entityFactory) { return new DynRealmDataBinderImpl(anyTypeDAO, dynRealmDAO, entityFactory, searchCondVisitor); @@ -863,7 +863,7 @@ public DynRealmDataBinder dynRealmDataBinder( @Bean public GroupDataBinder groupDataBinder( final EntityFactory entityFactory, - final SearchCondVisitor searchCondVisitor, + final AnySearchCondVisitor searchCondVisitor, final AnyUtilsFactory anyUtilsFactory, final AnyTypeDAO anyTypeDAO, final RealmSearchDAO realmSearchDAO, @@ -1035,7 +1035,7 @@ public ResourceDataBinder resourceDataBinder( @Bean public RoleDataBinder roleDataBinder( final EntityFactory entityFactory, - final SearchCondVisitor searchCondVisitor, + final AnySearchCondVisitor searchCondVisitor, final RealmSearchDAO realmSearchDAO, final DynRealmDAO dynRealmDAO, final RoleDAO roleDAO) { diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/DynRealmDataBinderImpl.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/DynRealmDataBinderImpl.java index e934692ad9b..5e1a2dc3c92 100644 --- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/DynRealmDataBinderImpl.java +++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/DynRealmDataBinderImpl.java @@ -29,8 +29,8 @@ import org.apache.syncope.core.persistence.api.entity.DynRealm; import org.apache.syncope.core.persistence.api.entity.DynRealmMembership; import org.apache.syncope.core.persistence.api.entity.EntityFactory; +import org.apache.syncope.core.persistence.api.search.AnySearchCondVisitor; import org.apache.syncope.core.persistence.api.search.SearchCondConverter; -import org.apache.syncope.core.persistence.api.search.SearchCondVisitor; import org.apache.syncope.core.provisioning.api.data.DynRealmDataBinder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -45,13 +45,13 @@ public class DynRealmDataBinderImpl implements DynRealmDataBinder { protected final EntityFactory entityFactory; - protected final SearchCondVisitor searchCondVisitor; + protected final AnySearchCondVisitor searchCondVisitor; public DynRealmDataBinderImpl( final AnyTypeDAO anyTypeDAO, final DynRealmDAO dynRealmDAO, final EntityFactory entityFactory, - final SearchCondVisitor searchCondVisitor) { + final AnySearchCondVisitor searchCondVisitor) { this.anyTypeDAO = anyTypeDAO; this.dynRealmDAO = dynRealmDAO; diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/FIQLQueryDataBinderImpl.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/FIQLQueryDataBinderImpl.java index 61ba15b93f3..748d0008e27 100644 --- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/FIQLQueryDataBinderImpl.java +++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/FIQLQueryDataBinderImpl.java @@ -27,8 +27,8 @@ import org.apache.syncope.core.persistence.api.dao.search.SearchCond; import org.apache.syncope.core.persistence.api.entity.EntityFactory; import org.apache.syncope.core.persistence.api.entity.FIQLQuery; +import org.apache.syncope.core.persistence.api.search.AnySearchCondVisitor; import org.apache.syncope.core.persistence.api.search.SearchCondConverter; -import org.apache.syncope.core.persistence.api.search.SearchCondVisitor; import org.apache.syncope.core.provisioning.api.data.FIQLQueryDataBinder; import org.apache.syncope.core.spring.security.AuthContextUtils; import org.slf4j.Logger; @@ -38,14 +38,14 @@ public class FIQLQueryDataBinderImpl implements FIQLQueryDataBinder { protected static final Logger LOG = LoggerFactory.getLogger(FIQLQueryDataBinder.class); - protected final SearchCondVisitor searchCondVisitor; + protected final AnySearchCondVisitor searchCondVisitor; protected final UserDAO userDAO; protected final EntityFactory entityFactory; public FIQLQueryDataBinderImpl( - final SearchCondVisitor searchCondVisitor, + final AnySearchCondVisitor searchCondVisitor, final UserDAO userDAO, final EntityFactory entityFactory) { diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/GroupDataBinderImpl.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/GroupDataBinderImpl.java index b459ea7ef3d..f0e5f243fca 100644 --- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/GroupDataBinderImpl.java +++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/GroupDataBinderImpl.java @@ -60,8 +60,8 @@ import org.apache.syncope.core.persistence.api.entity.group.TypeExtension; import org.apache.syncope.core.persistence.api.entity.user.UDynGroupMembership; import org.apache.syncope.core.persistence.api.entity.user.User; +import org.apache.syncope.core.persistence.api.search.AnySearchCondVisitor; import org.apache.syncope.core.persistence.api.search.SearchCondConverter; -import org.apache.syncope.core.persistence.api.search.SearchCondVisitor; import org.apache.syncope.core.provisioning.api.DerAttrHandler; import org.apache.syncope.core.provisioning.api.IntAttrNameParser; import org.apache.syncope.core.provisioning.api.MappingManager; @@ -75,7 +75,7 @@ @Transactional(rollbackFor = { Throwable.class }) public class GroupDataBinderImpl extends AnyDataBinder implements GroupDataBinder { - protected final SearchCondVisitor searchCondVisitor; + protected final AnySearchCondVisitor searchCondVisitor; public GroupDataBinderImpl( final AnyTypeDAO anyTypeDAO, @@ -93,7 +93,7 @@ public GroupDataBinderImpl( final MappingManager mappingManager, final IntAttrNameParser intAttrNameParser, final OutboundMatcher outboundMatcher, - final SearchCondVisitor searchCondVisitor, + final AnySearchCondVisitor searchCondVisitor, final PlainAttrValidationManager validator, final JexlTools jexlTools) { diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/RoleDataBinderImpl.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/RoleDataBinderImpl.java index 8081f36bcf6..288f12c4bae 100644 --- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/RoleDataBinderImpl.java +++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/RoleDataBinderImpl.java @@ -28,8 +28,8 @@ import org.apache.syncope.core.persistence.api.entity.EntityFactory; import org.apache.syncope.core.persistence.api.entity.Realm; import org.apache.syncope.core.persistence.api.entity.Role; +import org.apache.syncope.core.persistence.api.search.AnySearchCondVisitor; import org.apache.syncope.core.persistence.api.search.SearchCondConverter; -import org.apache.syncope.core.persistence.api.search.SearchCondVisitor; import org.apache.syncope.core.provisioning.api.data.RoleDataBinder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -46,14 +46,14 @@ public class RoleDataBinderImpl implements RoleDataBinder { protected final EntityFactory entityFactory; - protected final SearchCondVisitor searchCondVisitor; + protected final AnySearchCondVisitor searchCondVisitor; public RoleDataBinderImpl( final RealmSearchDAO realmSearchDAO, final DynRealmDAO dynRealmDAO, final RoleDAO roleDAO, final EntityFactory entityFactory, - final SearchCondVisitor searchCondVisitor) { + final AnySearchCondVisitor searchCondVisitor) { this.realmSearchDAO = realmSearchDAO; this.dynRealmDAO = dynRealmDAO; diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/notification/DefaultNotificationManager.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/notification/DefaultNotificationManager.java index 3fb10bea321..ee1c8e487ab 100644 --- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/notification/DefaultNotificationManager.java +++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/notification/DefaultNotificationManager.java @@ -59,8 +59,8 @@ import org.apache.syncope.core.persistence.api.entity.task.TaskExec; import org.apache.syncope.core.persistence.api.entity.user.UMembership; import org.apache.syncope.core.persistence.api.entity.user.User; +import org.apache.syncope.core.persistence.api.search.AnySearchCondVisitor; import org.apache.syncope.core.persistence.api.search.SearchCondConverter; -import org.apache.syncope.core.persistence.api.search.SearchCondVisitor; import org.apache.syncope.core.provisioning.api.DerAttrHandler; import org.apache.syncope.core.provisioning.api.IntAttrName; import org.apache.syncope.core.provisioning.api.IntAttrNameParser; @@ -112,7 +112,7 @@ public class DefaultNotificationManager implements NotificationManager { protected final IntAttrNameParser intAttrNameParser; - protected final SearchCondVisitor searchCondVisitor; + protected final AnySearchCondVisitor searchCondVisitor; protected final JexlTools jexlTools; @@ -134,7 +134,7 @@ public DefaultNotificationManager( final ConfParamOps confParamOps, final EntityFactory entityFactory, final IntAttrNameParser intAttrNameParser, - final SearchCondVisitor searchCondVisitor, + final AnySearchCondVisitor searchCondVisitor, final JexlTools jexlTools) { this.derSchemaDAO = derSchemaDAO; diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/InboundMatcher.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/InboundMatcher.java index 6970737e007..2b6a85a6a2a 100644 --- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/InboundMatcher.java +++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/InboundMatcher.java @@ -23,6 +23,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; import org.apache.commons.lang3.StringUtils; @@ -139,11 +140,11 @@ public Optional match( // first, attempt to match the provided connObjectLinkValue against the configured connObjectLink // (if available) via internal search if (StringUtils.isNotBlank(provision.getMapping().getConnObjectLink())) { - List found = anyUtilsFactory.getInstance(anyType.getKind()).dao(). - findByDerAttrValue( - provision.getMapping().getConnObjectLink(), - connObjectLinkValue, - provision.isIgnoreCaseMatch()); + List found = anySearchDAO.findByDerAttrValue( + provision.getMapping().getConnObjectLink(), + connObjectLinkValue, + provision.isIgnoreCaseMatch(), + anyType.getKind()); if (!found.isEmpty()) { return Optional.of(new InboundMatch(MatchType.ANY, found.getFirst())); } @@ -304,10 +305,11 @@ public List matchByConnObjectKeyValue( } case DERIVED -> - anys.addAll(anyUtils.dao().findByDerAttrValue( + anys.addAll(anySearchDAO.findByDerAttrValue( ((DerSchema) intAttrName.getSchema()).getExpression(), finalConnObjectKeyValue, - ignoreCaseMatch)); + ignoreCaseMatch, + anyTypeKind)); default -> { } @@ -435,10 +437,9 @@ public List match( public List match(final LiveSyncDelta syncDelta, final OrgUnit orgUnit) { String connObjectKey = null; - Optional connObjectKeyItem = orgUnit.getConnObjectKeyItem(); - if (connObjectKeyItem.isPresent()) { - Attribute connObjectKeyAttr = syncDelta.getObject(). - getAttributeByName(connObjectKeyItem.get().getExtAttrName()); + Item connObjectKeyItem = orgUnit.getConnObjectKeyItem().orElse(null); + if (connObjectKeyItem != null) { + Attribute connObjectKeyAttr = syncDelta.getObject().getAttributeByName(connObjectKeyItem.getExtAttrName()); if (connObjectKeyAttr != null) { connObjectKey = AttributeUtil.getStringValue(connObjectKeyAttr); } @@ -448,10 +449,10 @@ public List match(final LiveSyncDelta syncDelta, final OrgUnit orgUnit) { } for (ItemTransformer transformer - : MappingUtils.getItemTransformers(connObjectKeyItem.get(), getTransformers(connObjectKeyItem.get()))) { + : MappingUtils.getItemTransformers(connObjectKeyItem, getTransformers(connObjectKeyItem))) { List output = transformer.beforePull( - connObjectKeyItem.get(), + connObjectKeyItem, null, List.of(connObjectKey)); if (!CollectionUtils.isEmpty(output)) { @@ -459,27 +460,58 @@ public List match(final LiveSyncDelta syncDelta, final OrgUnit orgUnit) { } } + IntAttrName intAttrName; + try { + intAttrName = intAttrNameParser.parse(connObjectKeyItem.getIntAttrName()); + } catch (ParseException e) { + LOG.error("Invalid intAttrName '{}' specified, ignoring", connObjectKeyItem.getIntAttrName(), e); + return List.of(); + } + List result = new ArrayList<>(); - switch (connObjectKeyItem.get().getIntAttrName()) { - case "key" -> { - realmDAO.findById(connObjectKey).ifPresent(result::add); - } + if (intAttrName.getField() != null) { + switch (intAttrName.getField()) { + case "key" -> + realmDAO.findById(connObjectKey).ifPresent(result::add); - case "name" -> { - if (orgUnit.isIgnoreCaseMatch()) { - result.addAll(realmSearchDAO.findDescendants( - SyncopeConstants.ROOT_REALM, connObjectKey, Pageable.unpaged())); - } else { - result.addAll(realmSearchDAO.findByName(connObjectKey).stream().toList()); + case "name" -> { + if (orgUnit.isIgnoreCaseMatch()) { + AnyCond cond = new AnyCond(); + cond.setType(AttrCond.Type.IEQ); + cond.setSchema("name"); + cond.setExpression(connObjectKey); + result.addAll(realmSearchDAO.search( + Set.of(SyncopeConstants.ROOT_REALM), SearchCond.of(cond), Pageable.unpaged())); + } else { + result.addAll(realmSearchDAO.findByName(connObjectKey).stream().toList()); + } } - } - case "fullpath" -> { - realmSearchDAO.findByFullPath(connObjectKey).ifPresent(result::add); + case "fullpath" -> + realmSearchDAO.findByFullPath(connObjectKey).ifPresent(result::add); + + default -> { + } } + } else if (intAttrName.getSchemaType() != null) { + switch (intAttrName.getSchemaType()) { + case PLAIN -> { + AttrCond cond = new AttrCond(orgUnit.isIgnoreCaseMatch() ? AttrCond.Type.IEQ : AttrCond.Type.EQ); + cond.setSchema(intAttrName.getSchema().getKey()); + cond.setExpression(connObjectKey); + result.addAll(realmSearchDAO.search( + Set.of(SyncopeConstants.ROOT_REALM), SearchCond.of(cond), Pageable.unpaged())); + } + + case DERIVED -> + result.addAll(realmSearchDAO.findByDerAttrValue( + ((DerSchema) intAttrName.getSchema()).getExpression(), + connObjectKey, + orgUnit.isIgnoreCaseMatch())); - default -> { + default -> { + } } } diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/PushJobDelegate.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/PushJobDelegate.java index dac27921a17..fdcbc8989da 100644 --- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/PushJobDelegate.java +++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/PushJobDelegate.java @@ -43,8 +43,8 @@ import org.apache.syncope.core.persistence.api.entity.Realm; import org.apache.syncope.core.persistence.api.entity.policy.PushPolicy; import org.apache.syncope.core.persistence.api.entity.task.PushTask; +import org.apache.syncope.core.persistence.api.search.AnySearchCondVisitor; import org.apache.syncope.core.persistence.api.search.SearchCondConverter; -import org.apache.syncope.core.persistence.api.search.SearchCondVisitor; import org.apache.syncope.core.provisioning.api.ProvisionSorter; import org.apache.syncope.core.provisioning.api.job.JobExecutionContext; import org.apache.syncope.core.provisioning.api.job.JobExecutionException; @@ -60,7 +60,6 @@ import org.apache.syncope.core.spring.implementation.ImplementationManager; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; public class PushJobDelegate extends AbstractProvisioningJobDelegate @@ -73,7 +72,7 @@ public class PushJobDelegate protected RealmSearchDAO realmSearchDAO; @Autowired - protected SearchCondVisitor searchCondVisitor; + protected AnySearchCondVisitor searchCondVisitor; protected ProvisioningProfile profile; @@ -205,7 +204,7 @@ protected String doExecute(final JobExecutionContext context) throws JobExecutio // Never push the root realm List realms = realmSearchDAO.findDescendants( - profile.getTask().getSourceRealm().getFullPath(), null, Pageable.unpaged()).stream(). + profile.getTask().getSourceRealm().getFullPath(), null).stream(). filter(realm -> realm.getParent() != null).toList(); boolean result = true; for (int i = 0; i < realms.size() && result; i++) { @@ -231,8 +230,6 @@ protected String doExecute(final JobExecutionContext context) throws JobExecutio AnyType anyType = anyTypeDAO.findById(provision.getAnyType()). orElseThrow(() -> new NotFoundException("AnyType" + provision.getAnyType())); - AnyDAO anyDAO = anyUtilsFactory.getInstance(anyType.getKind()).dao(); - dispatcher.addHandlerSupplier(provision.getAnyType(), () -> { SyncopePushResultHandler handler; switch (anyType.getKind()) { @@ -254,7 +251,7 @@ protected String doExecute(final JobExecutionContext context) throws JobExecutio String filter = task.getFilter(anyType.getKey()).orElse(null); SearchCond cond = StringUtils.isBlank(filter) - ? anyDAO.getAllMatchingCond() + ? searchDAO.getAllMatchingCond() : SearchCondConverter.convert(searchCondVisitor, filter); long count = searchDAO.count( profile.getTask().getSourceRealm(), diff --git a/core/self-keymaster-starter/src/main/java/org/apache/syncope/core/starter/SelfKeymasterContext.java b/core/self-keymaster-starter/src/main/java/org/apache/syncope/core/starter/SelfKeymasterContext.java index bce05a8b9cc..fcc10a375f1 100644 --- a/core/self-keymaster-starter/src/main/java/org/apache/syncope/core/starter/SelfKeymasterContext.java +++ b/core/self-keymaster-starter/src/main/java/org/apache/syncope/core/starter/SelfKeymasterContext.java @@ -180,7 +180,7 @@ protected void addParameters(final List parameters) { ExternalDocumentation extDoc = new ExternalDocumentation(); extDoc.setDescription("Apache Syncope Reference Guide"); - extDoc.setUrl("https://syncope.apache.org/docs/3.0/reference-guide.html#domains"); + extDoc.setUrl("https://syncope.apache.org/docs/4.0/reference-guide.html#domains"); Schema schema = new Schema<>(); schema.setDescription("Domains are built to facilitate multitenancy."); diff --git a/ext/elasticsearch/client-elasticsearch/src/main/java/org/apache/syncope/ext/elasticsearch/client/ElasticsearchUtils.java b/ext/elasticsearch/client-elasticsearch/src/main/java/org/apache/syncope/ext/elasticsearch/client/ElasticsearchUtils.java index 7b773423767..260f5fd50ca 100644 --- a/ext/elasticsearch/client-elasticsearch/src/main/java/org/apache/syncope/ext/elasticsearch/client/ElasticsearchUtils.java +++ b/ext/elasticsearch/client-elasticsearch/src/main/java/org/apache/syncope/ext/elasticsearch/client/ElasticsearchUtils.java @@ -90,6 +90,17 @@ protected void relationships(final List> input, fin builder.put("relationshipTypes", relationshipTypes); } + protected void addPlainAttr(final Map builder, final List plainAttrs) { + for (PlainAttr plainAttr : plainAttrs) { + List values = plainAttr.getValues().stream(). + map(PlainAttrValue::getValue).collect(Collectors.toList()); + + Optional.ofNullable(plainAttr.getUniqueValue()).ifPresent(v -> values.add(v.getValue())); + + builder.put(plainAttr.getSchema(), values.size() == 1 ? values.getFirst() : values); + } + } + /** * Returns the document specialized with content from the provided any. * @@ -170,14 +181,7 @@ public Map document(final Any any) { } } - for (PlainAttr plainAttr : any.getPlainAttrs()) { - List values = plainAttr.getValues().stream(). - map(PlainAttrValue::getValue).collect(Collectors.toList()); - - Optional.ofNullable(plainAttr.getUniqueValue()).ifPresent(v -> values.add(v.getValue())); - - builder.put(plainAttr.getSchema(), values.size() == 1 ? values.getFirst() : values); - } + addPlainAttr(builder, any.getPlainAttrs()); // add also flattened membership attributes if (any instanceof Groupable groupable) { @@ -215,6 +219,7 @@ public Map document(final Realm realm) { builder.put("name", realm.getName()); builder.put("parent_id", realm.getParent() == null ? null : realm.getParent().getKey()); builder.put("fullPath", realm.getFullPath()); + addPlainAttr(builder, realm.getPlainAttrs()); customizeDocument(builder, realm); @@ -240,8 +245,6 @@ public Map document(final AuditEvent auditEvent) { return builder; } - protected void customizeDocument( - final Map builder, - final AuditEvent auditEvent) { + protected void customizeDocument(final Map builder, final AuditEvent auditEvent) { } } diff --git a/ext/elasticsearch/persistence/src/main/java/org/apache/syncope/core/persistence/elasticsearch/ElasticsearchPersistenceContext.java b/ext/elasticsearch/persistence/src/main/java/org/apache/syncope/core/persistence/elasticsearch/ElasticsearchPersistenceContext.java index 1c2b47ea20d..9672060f369 100644 --- a/ext/elasticsearch/persistence/src/main/java/org/apache/syncope/core/persistence/elasticsearch/ElasticsearchPersistenceContext.java +++ b/ext/elasticsearch/persistence/src/main/java/org/apache/syncope/core/persistence/elasticsearch/ElasticsearchPersistenceContext.java @@ -77,10 +77,19 @@ public AnySearchDAO anySearchDAO( @Bean public RealmSearchDAO realmSearchDAO( final @Lazy RealmDAO realmDAO, + final @Lazy PlainSchemaDAO plainSchemaDAO, + final EntityFactory entityFactory, + final PlainAttrValidationManager validator, final ElasticsearchClient client, final ElasticsearchProperties props) { - return new ElasticsearchRealmSearchDAO(realmDAO, client, props.getIndexMaxResultWindow()); + return new ElasticsearchRealmSearchDAO( + realmDAO, + plainSchemaDAO, + entityFactory, + validator, + client, + props.getIndexMaxResultWindow()); } @ConditionalOnMissingBean(name = "elasticsearchAuditEventDAO") diff --git a/ext/elasticsearch/persistence/src/main/java/org/apache/syncope/core/persistence/elasticsearch/dao/ElasticsearchAnySearchDAO.java b/ext/elasticsearch/persistence/src/main/java/org/apache/syncope/core/persistence/elasticsearch/dao/ElasticsearchAnySearchDAO.java index 0d415f731fc..82aee8ec6e6 100644 --- a/ext/elasticsearch/persistence/src/main/java/org/apache/syncope/core/persistence/elasticsearch/dao/ElasticsearchAnySearchDAO.java +++ b/ext/elasticsearch/persistence/src/main/java/org/apache/syncope/core/persistence/elasticsearch/dao/ElasticsearchAnySearchDAO.java @@ -144,7 +144,7 @@ protected AdminRealmsFilter getAdminRealmsFilter( orElseThrow(() -> new IllegalArgumentException("Invalid Realm full path: " + realmPath)); realmSearchDAO.findDescendants(realm.getFullPath(), base.getFullPath()). - forEach(descendant -> queries.add( + stream().map(Realm::getKey).forEach(descendant -> queries.add( new Query.Builder().term(QueryBuilders.term(). field("realm").value(descendant).caseInsensitive(false).build()). build())); @@ -594,8 +594,8 @@ protected Query getQuery(final AttrCond cond) { } @Override - protected CheckResult check(final AnyCond cond, final AnyTypeKind kind) { - CheckResult checked = super.check(cond, kind); + protected CheckResult check(final AnyCond cond, final Field field, final Set relationshipsFields) { + CheckResult checked = super.check(cond, field, relationshipsFields); // Manage difference between external id attribute and internal _id if ("id".equals(checked.cond().getSchema())) { @@ -615,7 +615,11 @@ protected Query getQuery(final AnyCond cond, final AnyTypeKind kind) { cond.setExpression(realm.getKey()); } - CheckResult checked = check(cond, kind); + CheckResult checked = check( + cond, + anyUtilsFactory.getInstance(kind).getField(cond.getSchema()). + orElseThrow(() -> new IllegalArgumentException("Invalid schema " + cond.getSchema())), + RELATIONSHIP_FIELDS); return fillAttrQuery(checked.schema(), checked.value(), checked.cond()); } diff --git a/ext/elasticsearch/persistence/src/main/java/org/apache/syncope/core/persistence/elasticsearch/dao/ElasticsearchRealmSearchDAO.java b/ext/elasticsearch/persistence/src/main/java/org/apache/syncope/core/persistence/elasticsearch/dao/ElasticsearchRealmSearchDAO.java index 77b9bebe0d0..93136df75cd 100644 --- a/ext/elasticsearch/persistence/src/main/java/org/apache/syncope/core/persistence/elasticsearch/dao/ElasticsearchRealmSearchDAO.java +++ b/ext/elasticsearch/persistence/src/main/java/org/apache/syncope/core/persistence/elasticsearch/dao/ElasticsearchRealmSearchDAO.java @@ -19,40 +19,63 @@ package org.apache.syncope.core.persistence.elasticsearch.dao; import co.elastic.clients.elasticsearch.ElasticsearchClient; +import co.elastic.clients.elasticsearch._types.FieldSort; +import co.elastic.clients.elasticsearch._types.FieldValue; import co.elastic.clients.elasticsearch._types.ScriptLanguage; import co.elastic.clients.elasticsearch._types.ScriptSortType; import co.elastic.clients.elasticsearch._types.ScriptSource; import co.elastic.clients.elasticsearch._types.SearchType; import co.elastic.clients.elasticsearch._types.SortOptions; import co.elastic.clients.elasticsearch._types.SortOrder; +import co.elastic.clients.elasticsearch._types.query_dsl.BoolQuery; import co.elastic.clients.elasticsearch._types.query_dsl.Query; import co.elastic.clients.elasticsearch._types.query_dsl.QueryBuilders; +import co.elastic.clients.elasticsearch._types.query_dsl.RangeQuery; import co.elastic.clients.elasticsearch.core.CountRequest; import co.elastic.clients.elasticsearch.core.SearchRequest; import co.elastic.clients.elasticsearch.core.search.Hit; import co.elastic.clients.elasticsearch.core.search.SourceConfig; +import co.elastic.clients.json.JsonData; +import java.lang.reflect.Field; import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.Set; +import java.util.stream.Stream; import org.apache.commons.lang3.StringUtils; import org.apache.syncope.common.lib.SyncopeConstants; +import org.apache.syncope.common.lib.types.AttrSchemaType; +import org.apache.syncope.core.persistence.api.attrvalue.PlainAttrValidationManager; import org.apache.syncope.core.persistence.api.dao.MalformedPathException; +import org.apache.syncope.core.persistence.api.dao.PlainSchemaDAO; import org.apache.syncope.core.persistence.api.dao.RealmDAO; import org.apache.syncope.core.persistence.api.dao.RealmSearchDAO; +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.EntityFactory; +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.persistence.api.utils.FormatUtils; +import org.apache.syncope.core.persistence.api.utils.RealmUtils; +import org.apache.syncope.core.persistence.common.dao.AbstractRealmSearchDAO; +import org.apache.syncope.core.persistence.common.dao.AbstractSearchDAO.CheckResult; import org.apache.syncope.core.spring.security.AuthContextUtils; import org.apache.syncope.ext.elasticsearch.client.ElasticsearchUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; import org.springframework.transaction.annotation.Transactional; -public class ElasticsearchRealmSearchDAO implements RealmSearchDAO { +public class ElasticsearchRealmSearchDAO extends AbstractRealmSearchDAO implements RealmSearchDAO { - protected static final Logger LOG = LoggerFactory.getLogger(RealmDAO.class); + protected static final Logger LOG = LoggerFactory.getLogger(RealmSearchDAO.class); - protected static final List REALM_SORT_OPTIONS = List.of( + protected static final Set ID_PROPS = Set.of("key", "id", "_id"); + + protected static final List FULLPATH_SORT_OPTIONS = List.of( new SortOptions.Builder(). script(s -> s.type(ScriptSortType.Number). script(t -> t.lang(ScriptLanguage.Painless). @@ -63,16 +86,23 @@ public class ElasticsearchRealmSearchDAO implements RealmSearchDAO { protected final RealmDAO realmDAO; + protected final RealmUtils realmUtils; + protected final ElasticsearchClient client; protected final int indexMaxResultWindow; public ElasticsearchRealmSearchDAO( final RealmDAO realmDAO, + final PlainSchemaDAO plainSchemaDAO, + final EntityFactory entityFactory, + final PlainAttrValidationManager validator, final ElasticsearchClient client, final int indexMaxResultWindow) { + super(plainSchemaDAO, entityFactory, validator); this.realmDAO = realmDAO; + this.realmUtils = new RealmUtils(entityFactory); this.client = client; this.indexMaxResultWindow = indexMaxResultWindow; } @@ -115,7 +145,7 @@ protected List search(final Query query) { index(ElasticsearchUtils.getRealmIndex(AuthContextUtils.getDomain())). searchType(SearchType.QueryThenFetch). query(query). - sort(REALM_SORT_OPTIONS). + sort(FULLPATH_SORT_OPTIONS). fields(List.of()).source(new SourceConfig.Builder().fetch(false).build()). build(); LOG.debug("Search request: {}", request); @@ -148,7 +178,7 @@ public List findChildren(final Realm realm) { flatMap(Optional::stream).map(Realm.class::cast).toList(); } - protected Query buildDescendantsQuery(final Set bases, final String keyword) { + protected Query buildDescendantsQuery(final Set bases, final SearchCond searchCond) { List basesQueries = new ArrayList<>(); bases.forEach(base -> { basesQueries.add(new Query.Builder().term(QueryBuilders.term(). @@ -159,28 +189,199 @@ protected Query buildDescendantsQuery(final Set bases, final String keyw }); Query prefix = new Query.Builder().disMax(QueryBuilders.disMax().queries(basesQueries).build()).build(); - if (keyword == null) { - return prefix; + BoolQuery.Builder boolBuilder = QueryBuilders.bool().filter(prefix); + if (searchCond != null) { + boolBuilder.filter(getQuery(searchCond)); } + return new Query.Builder().bool(boolBuilder.build()).build(); + } - return new Query.Builder().bool(QueryBuilders.bool().filter( - prefix, - new Query.Builder().wildcard(QueryBuilders.wildcard(). - field("name").value(keyword.replace('%', '*').replace("\\_", "_")). - caseInsensitive(true).build()).build()).build()). - build(); + protected Query getQuery(final SearchCond cond) { + Query query = null; + + switch (cond.getType()) { + case LEAF, NOT_LEAF -> { + query = cond.asLeaf(AnyCond.class).map(this::getQuery).orElse(null); + if (query == null) { + query = cond.asLeaf(AttrCond.class).map(this::getQuery).orElse(null); + } + if (query == null) { + query = getQueryForCustomConds(cond); + } + if (query == null) { + throw new IllegalArgumentException("Cannot construct QueryBuilder"); + } + if (cond.getType() == SearchCond.Type.NOT_LEAF) { + query = new Query.Builder().bool(QueryBuilders.bool().mustNot(query).build()).build(); + } + } + case AND -> { + List andCompound = new ArrayList<>(); + Query andLeft = getQuery(cond.getLeft()); + if (andLeft.isBool() && !andLeft.bool().filter().isEmpty()) { + andCompound.addAll(andLeft.bool().filter()); + } else { + andCompound.add(andLeft); + } + Query andRight = getQuery(cond.getRight()); + if (andRight.isBool() && !andRight.bool().filter().isEmpty()) { + andCompound.addAll(andRight.bool().filter()); + } else { + andCompound.add(andRight); + } + query = new Query.Builder().bool(QueryBuilders.bool().filter(andCompound).build()).build(); + } + case OR -> { + List orCompound = new ArrayList<>(); + Query orLeft = getQuery(cond.getLeft()); + if (orLeft.isDisMax()) { + orCompound.addAll(orLeft.disMax().queries()); + } else { + orCompound.add(orLeft); + } + Query orRight = getQuery(cond.getRight()); + if (orRight.isDisMax()) { + orCompound.addAll(orRight.disMax().queries()); + } else { + orCompound.add(orRight); + } + query = new Query.Builder().disMax(QueryBuilders.disMax().queries(orCompound).build()).build(); + } + default -> { + } + } + + return query; } @Override - public long countDescendants(final String base, final String keyword) { - return countDescendants(Set.of(base), keyword); + protected CheckResult check(final AnyCond cond, final Field field, final Set relationshipsFields) { + CheckResult checked = super.check(cond, field, relationshipsFields); + + // Manage difference between external id attribute and internal _id + if ("id".equals(checked.cond().getSchema())) { + checked.cond().setSchema("_id"); + } + if ("id".equals(checked.schema().getKey())) { + checked.schema().setKey("_id"); + } + + return checked; + } + + protected Query fillAttrQuery( + final PlainSchema schema, + final PlainAttrValue attrValue, + final AttrCond cond) { + + Object value = schema.getType() == AttrSchemaType.Date && attrValue.getDateValue() != null + ? FormatUtils.format(attrValue.getDateValue()) + : attrValue.getValue(); + + Query query = null; + switch (cond.getType()) { + case ISNOTNULL: + query = new Query.Builder().exists(QueryBuilders.exists().field(schema.getKey()).build()).build(); + break; + + case ISNULL: + query = new Query.Builder().bool(QueryBuilders.bool().mustNot( + new Query.Builder().exists(QueryBuilders.exists().field(schema.getKey()).build()) + .build()).build()).build(); + break; + + case ILIKE: + query = new Query.Builder().wildcard(QueryBuilders.wildcard(). + field(schema.getKey()).value(cond.getExpression().replace('%', '*').replace("\\_", "_")). + caseInsensitive(true).build()).build(); + break; + + case LIKE: + query = new Query.Builder().wildcard(QueryBuilders.wildcard(). + field(schema.getKey()).value(cond.getExpression().replace('%', '*').replace("\\_", "_")). + caseInsensitive(false).build()).build(); + break; + + case IEQ: + query = new Query.Builder().term(QueryBuilders.term(). + field(schema.getKey()).value(FieldValue.of(cond.getExpression())).caseInsensitive(true). + build()).build(); + break; + + case EQ: + FieldValue fieldValue = switch (value) { + case Double aDouble -> + FieldValue.of(aDouble); + case Long aLong -> + FieldValue.of(aLong); + case Boolean aBoolean -> + FieldValue.of(aBoolean); + default -> + FieldValue.of(value.toString()); + }; + query = new Query.Builder().term(QueryBuilders.term(). + field(schema.getKey()).value(fieldValue).caseInsensitive(false).build()). + build(); + break; + + case GE: + query = new Query.Builder().range(RangeQuery.of(r -> r.untyped(n -> n. + field(schema.getKey()). + gte(JsonData.of(value))))). + build(); + break; + + case GT: + query = new Query.Builder().range(RangeQuery.of(r -> r.untyped(n -> n. + field(schema.getKey()). + gt(JsonData.of(value))))). + build(); + break; + + case LE: + query = new Query.Builder().range(RangeQuery.of(r -> r.untyped(n -> n. + field(schema.getKey()). + lte(JsonData.of(value))))). + build(); + break; + + case LT: + query = new Query.Builder().range(RangeQuery.of(r -> r.untyped(n -> n. + field(schema.getKey()). + lt(JsonData.of(value))))). + build(); + break; + + default: + break; + } + + return query; + } + + protected Query getQuery(final AttrCond cond) { + CheckResult checked = check(cond); + return fillAttrQuery(checked.schema(), checked.value(), cond); + } + + protected Query getQuery(final AnyCond cond) { + CheckResult checked = check( + cond, + realmUtils.getField(cond.getSchema()). + orElseThrow(() -> new IllegalArgumentException("Invalid schema " + cond.getSchema())), + RELATIONSHIP_FIELDS); + return fillAttrQuery(checked.schema(), checked.value(), checked.cond()); + } + + protected Query getQueryForCustomConds(final SearchCond cond) { + return null; } @Override - public long countDescendants(final Set bases, final String keyword) { + protected long doCount(final Set bases, final SearchCond searchCond) { CountRequest request = new CountRequest.Builder(). index(ElasticsearchUtils.getRealmIndex(AuthContextUtils.getDomain())). - query(buildDescendantsQuery(bases, keyword)). + query(buildDescendantsQuery(bases, searchCond)). build(); LOG.debug("Count request: {}", request); @@ -192,20 +393,52 @@ public long countDescendants(final Set bases, final String keyword) { } } - @Override - public List findDescendants(final String base, final String keyword, final Pageable pageable) { - return findDescendants(Set.of(base), keyword, pageable); + protected List sortBuilders(final Stream orderBy) { + List options = new ArrayList<>(); + orderBy.forEach(clause -> { + String sortName = null; + + String fieldName = clause.getProperty(); + // Cannot sort by internal _id + if (!ID_PROPS.contains(fieldName)) { + Field anyField = realmUtils.getField(fieldName).orElse(null); + if (anyField == null) { + PlainSchema schema = plainSchemaDAO.findById(fieldName).orElse(null); + if (schema != null) { + sortName = fieldName; + } + } else { + sortName = fieldName; + } + } + + if (sortName == null) { + LOG.warn("Cannot build any valid clause from {}", clause); + } else { + if ("fullPath".equals(sortName)) { + options.addAll(FULLPATH_SORT_OPTIONS); + } else { + options.add(new SortOptions.Builder().field( + new FieldSort.Builder(). + field(sortName). + order(clause.getDirection() == Sort.Direction.ASC ? SortOrder.Asc : SortOrder.Desc). + build()). + build()); + } + } + }); + return options.isEmpty() ? FULLPATH_SORT_OPTIONS : options; } @Override - public List findDescendants(final Set bases, final String keyword, final Pageable pageable) { + protected List doSearch(final Set bases, final SearchCond searchCond, final Pageable pageable) { SearchRequest request = new SearchRequest.Builder(). index(ElasticsearchUtils.getRealmIndex(AuthContextUtils.getDomain())). searchType(SearchType.QueryThenFetch). - query(buildDescendantsQuery(bases, keyword)). + query(buildDescendantsQuery(bases, searchCond)). from(pageable.isUnpaged() ? 0 : pageable.getPageSize() * pageable.getPageNumber()). size(pageable.isUnpaged() ? indexMaxResultWindow : pageable.getPageSize()). - sort(REALM_SORT_OPTIONS). + sort(sortBuilders(pageable.getSort().get())). fields(List.of()).source(new SourceConfig.Builder().fetch(false).build()). build(); LOG.debug("Search request: {}", request); @@ -224,17 +457,22 @@ public List findDescendants(final Set bases, final String keyword } @Override - public List findDescendants(final String base, final String prefix) { - Query prefixQuery = new Query.Builder().disMax(QueryBuilders.disMax().queries( - new Query.Builder().term(QueryBuilders.term(). - field("fullPath").value(prefix).caseInsensitive(false).build()).build(), - new Query.Builder().prefix(QueryBuilders.prefix(). - field("fullPath").value(SyncopeConstants.ROOT_REALM.equals(prefix) ? "/" : prefix + "/"). - build()).build()).build()).build(); - - Query query = new Query.Builder().bool(QueryBuilders.bool().filter( - buildDescendantsQuery(Set.of(base), (String) null), prefixQuery).build()). - build(); + public List findDescendants(final String base, final String prefix) { + Query descendantsQuery = buildDescendantsQuery(Set.of(base), null); + Query query; + if (prefix == null) { + query = descendantsQuery; + } else { + Query prefixQuery = new Query.Builder().disMax(QueryBuilders.disMax().queries( + new Query.Builder().term(QueryBuilders.term(). + field("fullPath").value(prefix).caseInsensitive(false).build()).build(), + new Query.Builder().prefix(QueryBuilders.prefix(). + field("fullPath").value(SyncopeConstants.ROOT_REALM.equals(prefix) ? "/" : prefix + "/"). + build()).build()).build()).build(); + query = new Query.Builder().bool(QueryBuilders.bool().filter( + descendantsQuery, prefixQuery).build()). + build(); + } SearchRequest request = new SearchRequest.Builder(). index(ElasticsearchUtils.getRealmIndex(AuthContextUtils.getDomain())). @@ -242,7 +480,7 @@ public List findDescendants(final String base, final String prefix) { query(query). from(0). size(indexMaxResultWindow). - sort(REALM_SORT_OPTIONS). + sort(FULLPATH_SORT_OPTIONS). fields(List.of()).source(new SourceConfig.Builder().fetch(false).build()). build(); LOG.debug("Search request: {}", request); @@ -255,6 +493,6 @@ public List findDescendants(final String base, final String prefix) { } catch (Exception e) { LOG.error("While searching in Elasticsearch with request {}", request, e); } - return result; + return result.stream().map(realmDAO::findById).flatMap(Optional::stream).map(Realm.class::cast).toList(); } } diff --git a/ext/elasticsearch/persistence/src/test/java/org/apache/syncope/core/persistence/elasticsearch/dao/ElasticsearchAnySearchDAOTest.java b/ext/elasticsearch/persistence/src/test/java/org/apache/syncope/core/persistence/elasticsearch/dao/ElasticsearchAnySearchDAOTest.java index c7a3b524d2d..29be6a2919d 100644 --- a/ext/elasticsearch/persistence/src/test/java/org/apache/syncope/core/persistence/elasticsearch/dao/ElasticsearchAnySearchDAOTest.java +++ b/ext/elasticsearch/persistence/src/test/java/org/apache/syncope/core/persistence/elasticsearch/dao/ElasticsearchAnySearchDAOTest.java @@ -115,11 +115,12 @@ protected void setupSearchDAO() { public void getAdminRealmsFilter4realm() throws IOException { // 1. mock Realm root = mock(Realm.class); + when(root.getKey()).thenReturn("rootKey"); when(root.getFullPath()).thenReturn(SyncopeConstants.ROOT_REALM); when(realmSearchDAO.findByFullPath(SyncopeConstants.ROOT_REALM)).thenAnswer(ic -> Optional.of(root)); when(realmSearchDAO.findDescendants(eq(SyncopeConstants.ROOT_REALM), anyString())). - thenReturn(List.of("rootKey")); + thenReturn(List.of(root)); // 2. test Set adminRealms = Set.of(SyncopeConstants.ROOT_REALM); diff --git a/ext/elasticsearch/persistence/src/test/java/org/apache/syncope/core/persistence/elasticsearch/dao/ElasticsearchRealmSearchDAOTest.java b/ext/elasticsearch/persistence/src/test/java/org/apache/syncope/core/persistence/elasticsearch/dao/ElasticsearchRealmSearchDAOTest.java new file mode 100644 index 00000000000..eaf484f0915 --- /dev/null +++ b/ext/elasticsearch/persistence/src/test/java/org/apache/syncope/core/persistence/elasticsearch/dao/ElasticsearchRealmSearchDAOTest.java @@ -0,0 +1,206 @@ +/* + * 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.elasticsearch.dao; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +import co.elastic.clients.elasticsearch.ElasticsearchClient; +import co.elastic.clients.elasticsearch._types.FieldValue; +import co.elastic.clients.elasticsearch._types.query_dsl.BoolQuery; +import co.elastic.clients.elasticsearch._types.query_dsl.DisMaxQuery; +import co.elastic.clients.elasticsearch._types.query_dsl.Query; +import co.elastic.clients.elasticsearch._types.query_dsl.QueryBuilders; +import java.util.Optional; +import java.util.Set; +import org.apache.syncope.common.lib.SyncopeConstants; +import org.apache.syncope.common.lib.types.AttrSchemaType; +import org.apache.syncope.core.persistence.api.attrvalue.PlainAttrValidationManager; +import org.apache.syncope.core.persistence.api.dao.PlainSchemaDAO; +import org.apache.syncope.core.persistence.api.dao.RealmDAO; +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.EntityFactory; +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.JPAPlainSchema; +import org.apache.syncope.core.persistence.jpa.entity.JPARealm; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +public class ElasticsearchRealmSearchDAOTest { + + @Mock + private RealmDAO realmDAO; + + @Mock + private PlainSchemaDAO plainSchemaDAO; + + @Mock + private EntityFactory entityFactory; + + @Mock + private PlainAttrValidationManager validator; + + @Mock + private ElasticsearchClient client; + + private ElasticsearchRealmSearchDAO searchDAO; + + @BeforeEach + protected void setupSearchDAO() { + doReturn(JPARealm.class).when(entityFactory).realmClass(); + searchDAO = new ElasticsearchRealmSearchDAO( + realmDAO, + plainSchemaDAO, + entityFactory, + validator, + client, + 10000); + } + + @Test + public void query4anyCond() { + when(entityFactory.newEntity(PlainSchema.class)).thenReturn(new JPAPlainSchema()); + doAnswer(ic -> { + PlainAttrValue value = ic.getArgument(2); + value.setStringValue(ic.getArgument(1)); + return null; + }).when(validator).validate(any(PlainSchema.class), anyString(), any(PlainAttrValue.class)); + + AnyCond name = new AnyCond(AttrCond.Type.EQ); + name.setSchema("name"); + name.setExpression("two"); + + Query query = searchDAO.getQuery(SearchCond.of(name)); + assertThat( + new Query.Builder().term(QueryBuilders.term(). + field("name").value(FieldValue.of("two")).caseInsensitive(false).build()).build()). + usingRecursiveComparison().isEqualTo(query); + verifyNoInteractions(plainSchemaDAO); + } + + @Test + public void query4attrCond() { + PlainSchema aLong = new JPAPlainSchema(); + aLong.setKey("aLong"); + aLong.setType(AttrSchemaType.Long); + doReturn(Optional.of(aLong)).when(plainSchemaDAO).findById("aLong"); + doAnswer(ic -> { + PlainAttrValue value = ic.getArgument(2); + value.setLongValue(Long.valueOf(ic.getArgument(1))); + return null; + }).when(validator).validate(any(PlainSchema.class), anyString(), any(PlainAttrValue.class)); + + AttrCond attrEq = new AttrCond(AttrCond.Type.EQ); + attrEq.setSchema("aLong"); + attrEq.setExpression("42"); + + Query query = searchDAO.getQuery(SearchCond.of(attrEq)); + assertEquals(Query.Kind.Term, query._kind()); + assertEquals("aLong", query.term().field()); + assertEquals(Boolean.FALSE, query.term().caseInsensitive()); + assertEquals("42", query.term().value().anyValue().toString()); + } + + @Test + public void query4attrCondNullChecks() { + PlainSchema aLong = new JPAPlainSchema(); + aLong.setKey("aLong"); + aLong.setType(AttrSchemaType.Long); + doReturn(Optional.of(aLong)).when(plainSchemaDAO).findById("aLong"); + + AttrCond isNull = new AttrCond(AttrCond.Type.ISNULL); + isNull.setSchema("aLong"); + + Query query = searchDAO.getQuery(SearchCond.of(isNull)); + assertThat( + new Query.Builder().bool(QueryBuilders.bool().mustNot( + new Query.Builder().exists(QueryBuilders.exists().field("aLong").build()) + .build()).build()).build()). + usingRecursiveComparison().isEqualTo(query); + + AttrCond isNotNull = new AttrCond(AttrCond.Type.ISNOTNULL); + isNotNull.setSchema("aLong"); + + query = searchDAO.getQuery(SearchCond.of(isNotNull)); + assertThat( + new Query.Builder().exists(QueryBuilders.exists().field("aLong").build()).build()). + usingRecursiveComparison().isEqualTo(query); + } + + @Test + public void descendantsQueryWithAndOrFilters() { + when(entityFactory.newEntity(PlainSchema.class)).thenReturn(new JPAPlainSchema()); + doAnswer(ic -> { + PlainAttrValue value = ic.getArgument(2); + value.setStringValue(ic.getArgument(1)); + return null; + }).when(validator).validate(any(PlainSchema.class), anyString(), any(PlainAttrValue.class)); + + AnyCond nameTwo = new AnyCond(AttrCond.Type.EQ); + nameTwo.setSchema("name"); + nameTwo.setExpression("two"); + + AnyCond nameOdd = new AnyCond(AttrCond.Type.EQ); + nameOdd.setSchema("name"); + nameOdd.setExpression("odd"); + + Query query = searchDAO.buildDescendantsQuery( + Set.of(SyncopeConstants.ROOT_REALM), + SearchCond.or(SearchCond.of(nameTwo), SearchCond.of(nameOdd))); + + assertEquals(Query.Kind.Bool, query._kind()); + assertEquals(2, ((BoolQuery) query._get()).filter().size()); + Query right = ((BoolQuery) query._get()).filter().get(1); + assertEquals(Query.Kind.DisMax, right._kind()); + assertEquals(2, ((DisMaxQuery) right._get()).queries().size()); + + assertThat( + new Query.Builder().bool(QueryBuilders.bool(). + filter(new Query.Builder().disMax(QueryBuilders.disMax(). + queries(new Query.Builder().term(QueryBuilders.term(). + field("fullPath").value(FieldValue.of(SyncopeConstants.ROOT_REALM)). + caseInsensitive(false).build()).build()). + queries(new Query.Builder().regexp(QueryBuilders.regexp(). + field("fullPath").value("/.*").build()).build()). + build()).build()). + filter(new Query.Builder().disMax(QueryBuilders.disMax(). + queries(new Query.Builder().term(QueryBuilders.term(). + field("name").value(FieldValue.of("two")).caseInsensitive(false). + build()).build()). + queries(new Query.Builder().term(QueryBuilders.term(). + field("name").value(FieldValue.of("odd")).caseInsensitive(false). + build()).build()). + build()).build()). + build()).build()). + usingRecursiveComparison().isEqualTo(query); + } +} diff --git a/ext/opensearch/client-opensearch/src/main/java/org/apache/syncope/ext/opensearch/client/OpenSearchUtils.java b/ext/opensearch/client-opensearch/src/main/java/org/apache/syncope/ext/opensearch/client/OpenSearchUtils.java index 8fb81e6bab0..c9b9239632c 100644 --- a/ext/opensearch/client-opensearch/src/main/java/org/apache/syncope/ext/opensearch/client/OpenSearchUtils.java +++ b/ext/opensearch/client-opensearch/src/main/java/org/apache/syncope/ext/opensearch/client/OpenSearchUtils.java @@ -90,6 +90,17 @@ protected void relationships(final List> input, fin builder.put("relationshipTypes", relationshipTypes); } + protected void addPlainAttr(final Map builder, final List plainAttrs) { + for (PlainAttr plainAttr : plainAttrs) { + List values = plainAttr.getValues().stream(). + map(PlainAttrValue::getValue).collect(Collectors.toList()); + + Optional.ofNullable(plainAttr.getUniqueValue()).ifPresent(v -> values.add(v.getValue())); + + builder.put(plainAttr.getSchema(), values.size() == 1 ? values.getFirst() : values); + } + } + /** * Returns the document specialized with content from the provided any. * @@ -170,14 +181,7 @@ public Map document(final Any any) { } } - for (PlainAttr plainAttr : any.getPlainAttrs()) { - List values = plainAttr.getValues().stream(). - map(PlainAttrValue::getValue).collect(Collectors.toList()); - - Optional.ofNullable(plainAttr.getUniqueValue()).ifPresent(v -> values.add(v.getValue())); - - builder.put(plainAttr.getSchema(), values.size() == 1 ? values.getFirst() : values); - } + addPlainAttr(builder, any.getPlainAttrs()); // add also flattened membership attributes if (any instanceof Groupable groupable) { @@ -215,6 +219,7 @@ public Map document(final Realm realm) { builder.put("name", realm.getName()); builder.put("parent_id", realm.getParent() == null ? null : realm.getParent().getKey()); builder.put("fullPath", realm.getFullPath()); + addPlainAttr(builder, realm.getPlainAttrs()); customizeDocument(builder, realm); @@ -240,8 +245,6 @@ public Map document(final AuditEvent auditEvent) { return builder; } - protected void customizeDocument( - final Map builder, - final AuditEvent auditEvent) { + protected void customizeDocument(final Map builder, final AuditEvent auditEvent) { } } diff --git a/ext/opensearch/persistence/src/main/java/org/apache/syncope/core/persistence/opensearch/OpenSearchPersistenceContext.java b/ext/opensearch/persistence/src/main/java/org/apache/syncope/core/persistence/opensearch/OpenSearchPersistenceContext.java index 2ae69e63c4a..d3a48079c95 100644 --- a/ext/opensearch/persistence/src/main/java/org/apache/syncope/core/persistence/opensearch/OpenSearchPersistenceContext.java +++ b/ext/opensearch/persistence/src/main/java/org/apache/syncope/core/persistence/opensearch/OpenSearchPersistenceContext.java @@ -77,10 +77,19 @@ public AnySearchDAO anySearchDAO( @Bean public RealmSearchDAO realmSearchDAO( final @Lazy RealmDAO realmDAO, + final @Lazy PlainSchemaDAO plainSchemaDAO, + final EntityFactory entityFactory, + final PlainAttrValidationManager validator, final OpenSearchClient client, final OpenSearchProperties props) { - return new OpenSearchRealmSearchDAO(realmDAO, client, props.getIndexMaxResultWindow()); + return new OpenSearchRealmSearchDAO( + realmDAO, + plainSchemaDAO, + entityFactory, + validator, + client, + props.getIndexMaxResultWindow()); } @ConditionalOnMissingBean(name = "openSearchAuditEventDAO") diff --git a/ext/opensearch/persistence/src/main/java/org/apache/syncope/core/persistence/opensearch/dao/OpenSearchAnySearchDAO.java b/ext/opensearch/persistence/src/main/java/org/apache/syncope/core/persistence/opensearch/dao/OpenSearchAnySearchDAO.java index 82bad187ce9..e1e7294559c 100644 --- a/ext/opensearch/persistence/src/main/java/org/apache/syncope/core/persistence/opensearch/dao/OpenSearchAnySearchDAO.java +++ b/ext/opensearch/persistence/src/main/java/org/apache/syncope/core/persistence/opensearch/dao/OpenSearchAnySearchDAO.java @@ -143,7 +143,7 @@ protected AdminRealmsFilter getAdminRealmsFilter( orElseThrow(() -> new IllegalArgumentException("Invalid Realm full path: " + realmPath)); realmSearchDAO.findDescendants(realm.getFullPath(), base.getFullPath()). - forEach(descendant -> queries.add( + stream().map(Realm::getKey).forEach(descendant -> queries.add( new Query.Builder().term(QueryBuilders.term(). field("realm").value(FieldValue.of(descendant)).caseInsensitive(false).build()). build())); @@ -589,8 +589,8 @@ protected Query getQuery(final AttrCond cond) { } @Override - protected CheckResult check(final AnyCond cond, final AnyTypeKind kind) { - CheckResult checked = super.check(cond, kind); + protected CheckResult check(final AnyCond cond, final Field field, final Set relationshipsFields) { + CheckResult checked = super.check(cond, field, relationshipsFields); // Manage difference between external id attribute and internal _id if ("id".equals(checked.cond().getSchema())) { @@ -610,7 +610,11 @@ protected Query getQuery(final AnyCond cond, final AnyTypeKind kind) { cond.setExpression(realm.getKey()); } - CheckResult checked = check(cond, kind); + CheckResult checked = check( + cond, + anyUtilsFactory.getInstance(kind).getField(cond.getSchema()). + orElseThrow(() -> new IllegalArgumentException("Invalid schema " + cond.getSchema())), + RELATIONSHIP_FIELDS); return fillAttrQuery(checked.schema(), checked.value(), checked.cond()); } diff --git a/ext/opensearch/persistence/src/main/java/org/apache/syncope/core/persistence/opensearch/dao/OpenSearchRealmSearchDAO.java b/ext/opensearch/persistence/src/main/java/org/apache/syncope/core/persistence/opensearch/dao/OpenSearchRealmSearchDAO.java index c9b359196f7..253703f9f08 100644 --- a/ext/opensearch/persistence/src/main/java/org/apache/syncope/core/persistence/opensearch/dao/OpenSearchRealmSearchDAO.java +++ b/ext/opensearch/persistence/src/main/java/org/apache/syncope/core/persistence/opensearch/dao/OpenSearchRealmSearchDAO.java @@ -18,26 +18,43 @@ */ package org.apache.syncope.core.persistence.opensearch.dao; +import java.lang.reflect.Field; import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.Set; +import java.util.stream.Stream; import org.apache.commons.lang3.StringUtils; import org.apache.syncope.common.lib.SyncopeConstants; +import org.apache.syncope.common.lib.types.AttrSchemaType; +import org.apache.syncope.core.persistence.api.attrvalue.PlainAttrValidationManager; import org.apache.syncope.core.persistence.api.dao.MalformedPathException; +import org.apache.syncope.core.persistence.api.dao.PlainSchemaDAO; import org.apache.syncope.core.persistence.api.dao.RealmDAO; import org.apache.syncope.core.persistence.api.dao.RealmSearchDAO; +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.EntityFactory; +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.persistence.api.utils.FormatUtils; +import org.apache.syncope.core.persistence.api.utils.RealmUtils; +import org.apache.syncope.core.persistence.common.dao.AbstractRealmSearchDAO; import org.apache.syncope.core.spring.security.AuthContextUtils; import org.apache.syncope.ext.opensearch.client.OpenSearchUtils; +import org.opensearch.client.json.JsonData; import org.opensearch.client.opensearch.OpenSearchClient; import org.opensearch.client.opensearch._types.BuiltinScriptLanguage; +import org.opensearch.client.opensearch._types.FieldSort; import org.opensearch.client.opensearch._types.FieldValue; import org.opensearch.client.opensearch._types.ScriptLanguage; import org.opensearch.client.opensearch._types.ScriptSortType; import org.opensearch.client.opensearch._types.SearchType; import org.opensearch.client.opensearch._types.SortOptions; import org.opensearch.client.opensearch._types.SortOrder; +import org.opensearch.client.opensearch._types.query_dsl.BoolQuery; import org.opensearch.client.opensearch._types.query_dsl.Query; import org.opensearch.client.opensearch._types.query_dsl.QueryBuilders; import org.opensearch.client.opensearch.core.CountRequest; @@ -47,13 +64,16 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; import org.springframework.transaction.annotation.Transactional; -public class OpenSearchRealmSearchDAO implements RealmSearchDAO { +public class OpenSearchRealmSearchDAO extends AbstractRealmSearchDAO implements RealmSearchDAO { - protected static final Logger LOG = LoggerFactory.getLogger(RealmDAO.class); + protected static final Logger LOG = LoggerFactory.getLogger(RealmSearchDAO.class); - protected static final List REALM_SORT_OPTIONS = List.of( + protected static final Set ID_PROPS = Set.of("key", "id", "_id"); + + protected static final List FULLPATH_SORT_OPTIONS = List.of( new SortOptions.Builder(). script(s -> s.type(ScriptSortType.Number). script(t -> t.inline(i -> i.lang(ScriptLanguage.builder(). @@ -64,16 +84,23 @@ public class OpenSearchRealmSearchDAO implements RealmSearchDAO { protected final RealmDAO realmDAO; + protected final RealmUtils realmUtils; + protected final OpenSearchClient client; protected final int indexMaxResultWindow; public OpenSearchRealmSearchDAO( final RealmDAO realmDAO, + final PlainSchemaDAO plainSchemaDAO, + final EntityFactory entityFactory, + final PlainAttrValidationManager validator, final OpenSearchClient client, final int indexMaxResultWindow) { + super(plainSchemaDAO, entityFactory, validator); this.realmDAO = realmDAO; + this.realmUtils = new RealmUtils(entityFactory); this.client = client; this.indexMaxResultWindow = indexMaxResultWindow; } @@ -105,7 +132,7 @@ public Optional findByFullPath(final String fullPath) { orElse(null); return realmDAO.findById(result).map(Realm.class::cast); } catch (Exception e) { - LOG.error("While searching OpenSearch for Realm path {} with request {}", fullPath, request, e); + LOG.error("While searching Elasticsearch for Realm path {} with request {}", fullPath, request, e); } return Optional.empty(); @@ -116,7 +143,7 @@ protected List search(final Query query) { index(OpenSearchUtils.getRealmIndex(AuthContextUtils.getDomain())). searchType(SearchType.QueryThenFetch). query(query). - sort(REALM_SORT_OPTIONS). + sort(FULLPATH_SORT_OPTIONS). fields(List.of()).source(new SourceConfig.Builder().fetch(false).build()). build(); LOG.debug("Search request: {}", request); @@ -126,7 +153,7 @@ protected List search(final Query query) { map(Hit::id). toList(); } catch (Exception e) { - LOG.error("While searching in OpenSearch with request {}", request, e); + LOG.error("While searching in Elasticsearch with request {}", request, e); return List.of(); } } @@ -150,7 +177,7 @@ public List findChildren(final Realm realm) { flatMap(Optional::stream).map(Realm.class::cast).toList(); } - protected Query buildDescendantsQuery(final Set bases, final String keyword) { + protected Query buildDescendantsQuery(final Set bases, final SearchCond searchCond) { List basesQueries = new ArrayList<>(); bases.forEach(base -> { basesQueries.add(new Query.Builder().term(QueryBuilders.term(). @@ -161,53 +188,252 @@ protected Query buildDescendantsQuery(final Set bases, final String keyw }); Query prefix = new Query.Builder().disMax(QueryBuilders.disMax().queries(basesQueries).build()).build(); - if (keyword == null) { - return prefix; + BoolQuery.Builder boolBuilder = QueryBuilders.bool().filter(prefix); + if (searchCond != null) { + boolBuilder.filter(getQuery(searchCond)); } + return new Query.Builder().bool(boolBuilder.build()).build(); + } - return new Query.Builder().bool(QueryBuilders.bool().filter( - prefix, - new Query.Builder().wildcard(QueryBuilders.wildcard(). - field("name").value(keyword.replace('%', '*').replace("\\_", "_")). - caseInsensitive(true).build()).build()).build()). - build(); + protected Query getQuery(final SearchCond cond) { + Query query = null; + + switch (cond.getType()) { + case LEAF, NOT_LEAF -> { + query = cond.asLeaf(AnyCond.class).map(this::getQuery).orElse(null); + if (query == null) { + query = cond.asLeaf(AttrCond.class).map(this::getQuery).orElse(null); + } + if (query == null) { + query = getQueryForCustomConds(cond); + } + if (query == null) { + throw new IllegalArgumentException("Cannot construct QueryBuilder"); + } + if (cond.getType() == SearchCond.Type.NOT_LEAF) { + query = new Query.Builder().bool(QueryBuilders.bool().mustNot(query).build()).build(); + } + } + case AND -> { + List andCompound = new ArrayList<>(); + Query andLeft = getQuery(cond.getLeft()); + if (andLeft.isBool() && !andLeft.bool().filter().isEmpty()) { + andCompound.addAll(andLeft.bool().filter()); + } else { + andCompound.add(andLeft); + } + Query andRight = getQuery(cond.getRight()); + if (andRight.isBool() && !andRight.bool().filter().isEmpty()) { + andCompound.addAll(andRight.bool().filter()); + } else { + andCompound.add(andRight); + } + query = new Query.Builder().bool(QueryBuilders.bool().filter(andCompound).build()).build(); + } + case OR -> { + List orCompound = new ArrayList<>(); + Query orLeft = getQuery(cond.getLeft()); + if (orLeft.isDisMax()) { + orCompound.addAll(orLeft.disMax().queries()); + } else { + orCompound.add(orLeft); + } + Query orRight = getQuery(cond.getRight()); + if (orRight.isDisMax()) { + orCompound.addAll(orRight.disMax().queries()); + } else { + orCompound.add(orRight); + } + query = new Query.Builder().disMax(QueryBuilders.disMax().queries(orCompound).build()).build(); + } + default -> { + } + } + + return query; } @Override - public long countDescendants(final String base, final String keyword) { - return countDescendants(Set.of(base), keyword); + protected CheckResult check(final AnyCond cond, final Field field, final Set relationshipsFields) { + CheckResult checked = super.check(cond, field, relationshipsFields); + + // Manage difference between external id attribute and internal _id + if ("id".equals(checked.cond().getSchema())) { + checked.cond().setSchema("_id"); + } + if ("id".equals(checked.schema().getKey())) { + checked.schema().setKey("_id"); + } + + return checked; + } + + protected Query fillAttrQuery( + final PlainSchema schema, + final PlainAttrValue attrValue, + final AttrCond cond) { + + Object value = schema.getType() == AttrSchemaType.Date && attrValue.getDateValue() != null + ? FormatUtils.format(attrValue.getDateValue()) + : attrValue.getValue(); + + Query query = null; + switch (cond.getType()) { + case ISNOTNULL: + query = new Query.Builder().exists(QueryBuilders.exists().field(schema.getKey()).build()).build(); + break; + + case ISNULL: + query = new Query.Builder().bool(QueryBuilders.bool().mustNot( + new Query.Builder().exists(QueryBuilders.exists().field(schema.getKey()).build()) + .build()).build()).build(); + break; + + case ILIKE: + query = new Query.Builder().wildcard(QueryBuilders.wildcard(). + field(schema.getKey()).value(cond.getExpression().replace('%', '*').replace("\\_", "_")). + caseInsensitive(true).build()).build(); + break; + + case LIKE: + query = new Query.Builder().wildcard(QueryBuilders.wildcard(). + field(schema.getKey()).value(cond.getExpression().replace('%', '*').replace("\\_", "_")). + caseInsensitive(false).build()).build(); + break; + + case IEQ: + query = new Query.Builder().term(QueryBuilders.term(). + field(schema.getKey()).value(FieldValue.of(cond.getExpression())).caseInsensitive(true). + build()).build(); + break; + + case EQ: + FieldValue fieldValue = switch (value) { + case Double aDouble -> + FieldValue.of(aDouble); + case Long aLong -> + FieldValue.of(aLong); + case Boolean aBoolean -> + FieldValue.of(aBoolean); + default -> + FieldValue.of(value.toString()); + }; + query = new Query.Builder().term(QueryBuilders.term(). + field(schema.getKey()).value(fieldValue).caseInsensitive(false).build()). + build(); + break; + + case GE: + query = new Query.Builder().range(QueryBuilders.range(). + field(schema.getKey()).gte(JsonData.of(value)).build()). + build(); + break; + + case GT: + query = new Query.Builder().range(QueryBuilders.range(). + field(schema.getKey()).gt(JsonData.of(value)).build()). + build(); + break; + + case LE: + query = new Query.Builder().range(QueryBuilders.range(). + field(schema.getKey()).lte(JsonData.of(value)).build()). + build(); + break; + + case LT: + query = new Query.Builder().range(QueryBuilders.range(). + field(schema.getKey()).lt(JsonData.of(value)).build()). + build(); + break; + + default: + break; + } + + return query; + } + + protected Query getQuery(final AttrCond cond) { + CheckResult checked = check(cond); + return fillAttrQuery(checked.schema(), checked.value(), cond); + } + + protected Query getQuery(final AnyCond cond) { + CheckResult checked = check( + cond, + realmUtils.getField(cond.getSchema()). + orElseThrow(() -> new IllegalArgumentException("Invalid schema " + cond.getSchema())), + RELATIONSHIP_FIELDS); + return fillAttrQuery(checked.schema(), checked.value(), checked.cond()); + } + + protected Query getQueryForCustomConds(final SearchCond cond) { + return null; } @Override - public long countDescendants(final Set bases, final String keyword) { + protected long doCount(final Set bases, final SearchCond searchCond) { CountRequest request = new CountRequest.Builder(). index(OpenSearchUtils.getRealmIndex(AuthContextUtils.getDomain())). - query(buildDescendantsQuery(bases, keyword)). + query(buildDescendantsQuery(bases, searchCond)). build(); LOG.debug("Count request: {}", request); try { return client.count(request).count(); } catch (Exception e) { - LOG.error("While counting in OpenSearch with request {}", request, e); + LOG.error("While counting in Elasticsearch with request {}", request, e); return 0; } } - @Override - public List findDescendants(final String base, final String keyword, final Pageable pageable) { - return findDescendants(Set.of(base), keyword, pageable); + protected List sortBuilders(final Stream orderBy) { + List options = new ArrayList<>(); + orderBy.forEach(clause -> { + String sortName = null; + + String fieldName = clause.getProperty(); + // Cannot sort by internal _id + if (!ID_PROPS.contains(fieldName)) { + Field anyField = realmUtils.getField(fieldName).orElse(null); + if (anyField == null) { + PlainSchema schema = plainSchemaDAO.findById(fieldName).orElse(null); + if (schema != null) { + sortName = fieldName; + } + } else { + sortName = fieldName; + } + } + + if (sortName == null) { + LOG.warn("Cannot build any valid clause from {}", clause); + } else { + if ("fullPath".equals(sortName)) { + options.addAll(FULLPATH_SORT_OPTIONS); + } else { + options.add(new SortOptions.Builder().field( + new FieldSort.Builder(). + field(sortName). + order(clause.getDirection() == Sort.Direction.ASC ? SortOrder.Asc : SortOrder.Desc). + build()). + build()); + } + } + }); + return options.isEmpty() ? FULLPATH_SORT_OPTIONS : options; } @Override - public List findDescendants(final Set bases, final String keyword, final Pageable pageable) { + protected List doSearch(final Set bases, final SearchCond searchCond, final Pageable pageable) { SearchRequest request = new SearchRequest.Builder(). index(OpenSearchUtils.getRealmIndex(AuthContextUtils.getDomain())). searchType(SearchType.QueryThenFetch). - query(buildDescendantsQuery(bases, keyword)). + query(buildDescendantsQuery(bases, searchCond)). from(pageable.isUnpaged() ? 0 : pageable.getPageSize() * pageable.getPageNumber()). size(pageable.isUnpaged() ? indexMaxResultWindow : pageable.getPageSize()). - sort(REALM_SORT_OPTIONS). + sort(sortBuilders(pageable.getSort().get())). fields(List.of()).source(new SourceConfig.Builder().fetch(false).build()). build(); LOG.debug("Search request: {}", request); @@ -218,7 +444,7 @@ public List findDescendants(final Set bases, final String keyword map(Hit::id). toList(); } catch (Exception e) { - LOG.error("While searching in OpenSearch with request {}", request, e); + LOG.error("While searching in Elasticsearch with request {}", request, e); } return result.stream().map(realmDAO::findById). @@ -226,17 +452,22 @@ public List findDescendants(final Set bases, final String keyword } @Override - public List findDescendants(final String base, final String prefix) { - Query prefixQuery = new Query.Builder().disMax(QueryBuilders.disMax().queries( - new Query.Builder().term(QueryBuilders.term(). - field("fullPath").value(FieldValue.of(prefix)).caseInsensitive(false).build()).build(), - new Query.Builder().prefix(QueryBuilders.prefix(). - field("fullPath").value(SyncopeConstants.ROOT_REALM.equals(prefix) ? "/" : prefix + "/"). - build()).build()).build()).build(); - - Query query = new Query.Builder().bool(QueryBuilders.bool().filter( - buildDescendantsQuery(Set.of(base), (String) null), prefixQuery).build()). - build(); + public List findDescendants(final String base, final String prefix) { + Query descendantsQuery = buildDescendantsQuery(Set.of(base), null); + Query query; + if (prefix == null) { + query = descendantsQuery; + } else { + Query prefixQuery = new Query.Builder().disMax(QueryBuilders.disMax().queries( + new Query.Builder().term(QueryBuilders.term(). + field("fullPath").value(FieldValue.of(prefix)).caseInsensitive(false).build()).build(), + new Query.Builder().prefix(QueryBuilders.prefix(). + field("fullPath").value(SyncopeConstants.ROOT_REALM.equals(prefix) ? "/" : prefix + "/"). + build()).build()).build()).build(); + query = new Query.Builder().bool(QueryBuilders.bool().filter( + descendantsQuery, prefixQuery).build()). + build(); + } SearchRequest request = new SearchRequest.Builder(). index(OpenSearchUtils.getRealmIndex(AuthContextUtils.getDomain())). @@ -244,7 +475,7 @@ public List findDescendants(final String base, final String prefix) { query(query). from(0). size(indexMaxResultWindow). - sort(REALM_SORT_OPTIONS). + sort(FULLPATH_SORT_OPTIONS). fields(List.of()).source(new SourceConfig.Builder().fetch(false).build()). build(); LOG.debug("Search request: {}", request); @@ -255,8 +486,8 @@ public List findDescendants(final String base, final String prefix) { map(Hit::id). toList(); } catch (Exception e) { - LOG.error("While searching in OpenSearch", e); + LOG.error("While searching in Elasticsearch with request {}", request, e); } - return result; + return result.stream().map(realmDAO::findById).flatMap(Optional::stream).map(Realm.class::cast).toList(); } } diff --git a/ext/opensearch/persistence/src/test/java/org/apache/syncope/core/persistence/opensearch/dao/OpenSearchAnySearchDAOTest.java b/ext/opensearch/persistence/src/test/java/org/apache/syncope/core/persistence/opensearch/dao/OpenSearchAnySearchDAOTest.java index 5160c955f1a..f8fe2123061 100644 --- a/ext/opensearch/persistence/src/test/java/org/apache/syncope/core/persistence/opensearch/dao/OpenSearchAnySearchDAOTest.java +++ b/ext/opensearch/persistence/src/test/java/org/apache/syncope/core/persistence/opensearch/dao/OpenSearchAnySearchDAOTest.java @@ -116,11 +116,12 @@ protected void setupSearchDAO() { public void getAdminRealmsFilter4realm() throws IOException { // 1. mock Realm root = mock(Realm.class); + when(root.getKey()).thenReturn("rootKey"); when(root.getFullPath()).thenReturn(SyncopeConstants.ROOT_REALM); when(realmSearchDAO.findByFullPath(SyncopeConstants.ROOT_REALM)).thenAnswer(ic -> Optional.of(root)); when(realmSearchDAO.findDescendants(eq(SyncopeConstants.ROOT_REALM), anyString())). - thenReturn(List.of("rootKey")); + thenReturn(List.of(root)); // 2. test Set adminRealms = Set.of(SyncopeConstants.ROOT_REALM); diff --git a/ext/opensearch/persistence/src/test/java/org/apache/syncope/core/persistence/opensearch/dao/OpenSearchRealmSearchDAOTest.java b/ext/opensearch/persistence/src/test/java/org/apache/syncope/core/persistence/opensearch/dao/OpenSearchRealmSearchDAOTest.java new file mode 100644 index 00000000000..571aba0df53 --- /dev/null +++ b/ext/opensearch/persistence/src/test/java/org/apache/syncope/core/persistence/opensearch/dao/OpenSearchRealmSearchDAOTest.java @@ -0,0 +1,206 @@ +/* + * 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.opensearch.dao; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +import java.util.Optional; +import java.util.Set; +import org.apache.syncope.common.lib.SyncopeConstants; +import org.apache.syncope.common.lib.types.AttrSchemaType; +import org.apache.syncope.core.persistence.api.attrvalue.PlainAttrValidationManager; +import org.apache.syncope.core.persistence.api.dao.PlainSchemaDAO; +import org.apache.syncope.core.persistence.api.dao.RealmDAO; +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.EntityFactory; +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.JPAPlainSchema; +import org.apache.syncope.core.persistence.jpa.entity.JPARealm; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.opensearch.client.opensearch.OpenSearchClient; +import org.opensearch.client.opensearch._types.FieldValue; +import org.opensearch.client.opensearch._types.query_dsl.BoolQuery; +import org.opensearch.client.opensearch._types.query_dsl.DisMaxQuery; +import org.opensearch.client.opensearch._types.query_dsl.Query; +import org.opensearch.client.opensearch._types.query_dsl.QueryBuilders; + +@ExtendWith(MockitoExtension.class) +public class OpenSearchRealmSearchDAOTest { + + @Mock + private RealmDAO realmDAO; + + @Mock + private PlainSchemaDAO plainSchemaDAO; + + @Mock + private EntityFactory entityFactory; + + @Mock + private PlainAttrValidationManager validator; + + @Mock + private OpenSearchClient client; + + private OpenSearchRealmSearchDAO searchDAO; + + @BeforeEach + protected void setupSearchDAO() { + doReturn(JPARealm.class).when(entityFactory).realmClass(); + searchDAO = new OpenSearchRealmSearchDAO( + realmDAO, + plainSchemaDAO, + entityFactory, + validator, + client, + 10000); + } + + @Test + public void query4anyCond() { + when(entityFactory.newEntity(PlainSchema.class)).thenReturn(new JPAPlainSchema()); + doAnswer(ic -> { + PlainAttrValue value = ic.getArgument(2); + value.setStringValue(ic.getArgument(1)); + return null; + }).when(validator).validate(any(PlainSchema.class), anyString(), any(PlainAttrValue.class)); + + AnyCond name = new AnyCond(AttrCond.Type.EQ); + name.setSchema("name"); + name.setExpression("two"); + + Query query = searchDAO.getQuery(SearchCond.of(name)); + assertThat( + new Query.Builder().term(QueryBuilders.term(). + field("name").value(FieldValue.of("two")).caseInsensitive(false).build()).build()). + usingRecursiveComparison().isEqualTo(query); + verifyNoInteractions(plainSchemaDAO); + } + + @Test + public void query4attrCond() { + PlainSchema aLong = new JPAPlainSchema(); + aLong.setKey("aLong"); + aLong.setType(AttrSchemaType.Long); + doReturn(Optional.of(aLong)).when(plainSchemaDAO).findById("aLong"); + doAnswer(ic -> { + PlainAttrValue value = ic.getArgument(2); + value.setLongValue(Long.valueOf(ic.getArgument(1))); + return null; + }).when(validator).validate(any(PlainSchema.class), anyString(), any(PlainAttrValue.class)); + + AttrCond attrEq = new AttrCond(AttrCond.Type.EQ); + attrEq.setSchema("aLong"); + attrEq.setExpression("42"); + + Query query = searchDAO.getQuery(SearchCond.of(attrEq)); + assertEquals(Query.Kind.Term, query._kind()); + assertEquals("aLong", query.term().field()); + assertEquals(Boolean.FALSE, query.term().caseInsensitive()); + assertEquals(42, query.term().value().longValue()); + } + + @Test + public void query4attrCondNullChecks() { + PlainSchema aLong = new JPAPlainSchema(); + aLong.setKey("aLong"); + aLong.setType(AttrSchemaType.Long); + doReturn(Optional.of(aLong)).when(plainSchemaDAO).findById("aLong"); + + AttrCond isNull = new AttrCond(AttrCond.Type.ISNULL); + isNull.setSchema("aLong"); + + Query query = searchDAO.getQuery(SearchCond.of(isNull)); + assertThat( + new Query.Builder().bool(QueryBuilders.bool().mustNot( + new Query.Builder().exists(QueryBuilders.exists().field("aLong").build()) + .build()).build()).build()). + usingRecursiveComparison().isEqualTo(query); + + AttrCond isNotNull = new AttrCond(AttrCond.Type.ISNOTNULL); + isNotNull.setSchema("aLong"); + + query = searchDAO.getQuery(SearchCond.of(isNotNull)); + assertThat( + new Query.Builder().exists(QueryBuilders.exists().field("aLong").build()).build()). + usingRecursiveComparison().isEqualTo(query); + } + + @Test + public void descendantsQueryWithAndOrFilters() { + when(entityFactory.newEntity(PlainSchema.class)).thenReturn(new JPAPlainSchema()); + doAnswer(ic -> { + PlainAttrValue value = ic.getArgument(2); + value.setStringValue(ic.getArgument(1)); + return null; + }).when(validator).validate(any(PlainSchema.class), anyString(), any(PlainAttrValue.class)); + + AnyCond nameTwo = new AnyCond(AttrCond.Type.EQ); + nameTwo.setSchema("name"); + nameTwo.setExpression("two"); + + AnyCond nameOdd = new AnyCond(AttrCond.Type.EQ); + nameOdd.setSchema("name"); + nameOdd.setExpression("odd"); + + Query query = searchDAO.buildDescendantsQuery( + Set.of(SyncopeConstants.ROOT_REALM), + SearchCond.or(SearchCond.of(nameTwo), SearchCond.of(nameOdd))); + + assertEquals(Query.Kind.Bool, query._kind()); + assertEquals(2, ((BoolQuery) query._get()).filter().size()); + Query right = ((BoolQuery) query._get()).filter().get(1); + assertEquals(Query.Kind.DisMax, right._kind()); + assertEquals(2, ((DisMaxQuery) right._get()).queries().size()); + + assertThat( + new Query.Builder().bool(QueryBuilders.bool(). + filter(new Query.Builder().disMax(QueryBuilders.disMax(). + queries(new Query.Builder().term(QueryBuilders.term(). + field("fullPath").value(FieldValue.of(SyncopeConstants.ROOT_REALM)). + caseInsensitive(false).build()).build()). + queries(new Query.Builder().regexp(QueryBuilders.regexp(). + field("fullPath").value("/.*").build()).build()). + build()).build()). + filter(new Query.Builder().disMax(QueryBuilders.disMax(). + queries(new Query.Builder().term(QueryBuilders.term(). + field("name").value(FieldValue.of("two")).caseInsensitive(false). + build()).build()). + queries(new Query.Builder().term(QueryBuilders.term(). + field("name").value(FieldValue.of("odd")).caseInsensitive(false). + build()).build()). + build()).build()). + build()).build()). + usingRecursiveComparison().isEqualTo(query); + } +} diff --git a/fit/core-reference/src/main/java/org/apache/syncope/fit/core/reference/TestCommand.java b/fit/core-reference/src/main/java/org/apache/syncope/fit/core/reference/TestCommand.java index a43bb8f0956..8361c248692 100644 --- a/fit/core-reference/src/main/java/org/apache/syncope/fit/core/reference/TestCommand.java +++ b/fit/core-reference/src/main/java/org/apache/syncope/fit/core/reference/TestCommand.java @@ -46,7 +46,7 @@ public class TestCommand implements Command { private AnyObjectLogic anyObjectLogic; private Optional getRealm(final String fullPath) { - return realmLogic.search(null, Set.of(fullPath), Pageable.unpaged()).get(). + return realmLogic.search(Set.of(fullPath), null, Pageable.unpaged()).get(). filter(realm -> fullPath.equals(realm.getFullPath())).findFirst(); } diff --git a/fit/core-reference/src/test/java/org/apache/syncope/fit/AbstractITCase.java b/fit/core-reference/src/test/java/org/apache/syncope/fit/AbstractITCase.java index 297bb64e5c0..497135e169f 100644 --- a/fit/core-reference/src/test/java/org/apache/syncope/fit/AbstractITCase.java +++ b/fit/core-reference/src/test/java/org/apache/syncope/fit/AbstractITCase.java @@ -501,11 +501,11 @@ public static void anonymousSetup() throws IOException { IS_FLOWABLE_ENABLED = uwfAdapter.get("resource").asText().contains("Flowable"); JsonNode anySearchDAO = beans.findValues("anySearchDAO").getFirst(); - IS_ELASTICSEARCH_ENABLED = anySearchDAO.get("type").asText().contains("Elasticsearch"); - IS_OPENSEARCH_ENABLED = anySearchDAO.get("type").asText().contains("OpenSearch"); + IS_ELASTICSEARCH_ENABLED = anySearchDAO.get("resource").asText().contains("Elasticsearch"); + IS_OPENSEARCH_ENABLED = anySearchDAO.get("resource").asText().contains("OpenSearch"); IS_EXT_SEARCH_ENABLED = IS_ELASTICSEARCH_ENABLED || IS_OPENSEARCH_ENABLED; - IS_NEO4J_PERSISTENCE = anySearchDAO.get("type").asText().contains("Neo4j"); + IS_NEO4J_PERSISTENCE = anySearchDAO.get("resource").asText().contains("neo4j"); if (!IS_EXT_SEARCH_ENABLED) { return; @@ -977,7 +977,7 @@ protected static OIDCRPClientAppTO buildOIDCRP() { oidcrpTO.setAuthPolicy(authPolicyTO.getKey()); oidcrpTO.setAccessPolicy(accessPolicyTO.getKey()); - + oidcrpTO.setAccessTokenMaxActiveTokens(0L); oidcrpTO.setAccessTokenMaxTimeToLive("PT8H"); oidcrpTO.setAccessTokenTimeToKill("PT2H"); diff --git a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/MultitenancyITCase.java b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/MultitenancyITCase.java index dd4d98ee80e..5d27d7b3697 100644 --- a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/MultitenancyITCase.java +++ b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/MultitenancyITCase.java @@ -103,7 +103,7 @@ public void readPlainSchemas() { @Test public void readRealm() { PagedResult realms = ADMIN_CLIENT.getService(RealmService.class). - search(new RealmQuery.Builder().keyword("*").build()); + search(new RealmQuery.Builder().build()); assertEquals(1, realms.getTotalCount()); assertEquals(1, realms.getResult().size()); assertEquals(SyncopeConstants.ROOT_REALM, realms.getResult().getFirst().getName()); @@ -112,7 +112,7 @@ public void readRealm() { @Test public void createUser() { assertNull(ADMIN_CLIENT.getService(RealmService.class). - search(new RealmQuery.Builder().keyword("*").build()).getResult().getFirst().getPasswordPolicy()); + search(new RealmQuery.Builder().build()).getResult().getFirst().getPasswordPolicy()); UserCR userCR = new UserCR(); userCR.setRealm(SyncopeConstants.ROOT_REALM); diff --git a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/RealmITCase.java b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/RealmITCase.java index 987780c3acb..c7a8ddae5ca 100644 --- a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/RealmITCase.java +++ b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/RealmITCase.java @@ -23,6 +23,7 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; @@ -68,8 +69,166 @@ public class RealmITCase extends AbstractITCase { @Test public void search() { - PagedResult match = REALM_SERVICE.search(new RealmQuery.Builder().keyword("*o*").build()); + PagedResult match = REALM_SERVICE.search(new RealmQuery.Builder().fiql("name=~*o*").build()); assertTrue(match.getResult().stream().allMatch(realm -> realm.getName().contains("o"))); + + RealmTO realm = new RealmTO(); + realm.setName("searchTest1"); + realm.getAnyTypeClasses().add("other"); + realm.getPlainAttrs().add(new Attr.Builder("ctype").value("number1").build()); + realm.getPlainAttrs().add(new Attr.Builder("aLong").value("42").build()); + realm.getPlainAttrs().add(new Attr.Builder("loginDate").value("2008-05-26").build()); + realm.getResources().add(RESOURCE_NAME_LDAP_ORGUNIT); + REALM_SERVICE.create(SyncopeConstants.ROOT_REALM, realm); + + realm = new RealmTO(); + realm.setName("searchTest2"); + realm.getAnyTypeClasses().add("other"); + realm.getPlainAttrs().add(new Attr.Builder("ctype").value("string2").build()); + realm.getPlainAttrs().add(new Attr.Builder("aLong").value("90").build()); + realm.getPlainAttrs().add(new Attr.Builder("loginDate").value("2009-05-26").build()); + realm.getResources().add(RESOURCE_NAME_LDAP_ORGUNIT); + REALM_SERVICE.create("/even", realm); + + realm = new RealmTO(); + realm.setName("searchTest3"); + realm.getAnyTypeClasses().add("other"); + realm.getPlainAttrs().add(new Attr.Builder("ctype").value("string2").build()); + realm.getPlainAttrs().add(new Attr.Builder("aLong").value("42").build()); + realm.getPlainAttrs().add(new Attr.Builder("loginDate").value("2009-05-26").value("2010-05-26").build()); + realm.getResources().add(RESOURCE_NAME_LDAP_ORGUNIT); + REALM_SERVICE.create("/even", realm); + + realm = new RealmTO(); + realm.setName("searchTest4"); + realm.getAnyTypeClasses().add("other"); + realm.getPlainAttrs().add(new Attr.Builder("aLong").value("4242").build()); + realm.getResources().add(RESOURCE_NAME_LDAP_ORGUNIT); + REALM_SERVICE.create("/even/two", realm); + + realm = new RealmTO(); + realm.setName("searchTest5"); + realm.getAnyTypeClasses().add("other"); + realm.getPlainAttrs().add(new Attr.Builder("ctype").value("String5").build()); + realm.getPlainAttrs().add(new Attr.Builder("aLong").value("5").build()); + realm.getPlainAttrs().add(new Attr.Builder("loginDate").value("2011-05-26").build()); + realm.getResources().add(RESOURCE_NAME_LDAP_ORGUNIT); + REALM_SERVICE.create("/even", realm); + + // Numeric equality + match = REALM_SERVICE.search(new RealmQuery.Builder().base("/even").fiql("aLong==90").build()); + assertEquals(1, match.getSize()); + assertTrue(match.getResult().stream().allMatch(r -> r.getName().equals("searchTest2"))); + + match = REALM_SERVICE.search(new RealmQuery.Builder().fiql("aLong==42").orderBy("name DESC").build()); + assertEquals(2, match.getSize()); + assertEquals("searchTest3", match.getResult().get(0).getName()); + assertEquals("searchTest1", match.getResult().get(1).getName()); + + // Mixed numeric + string filters + match = REALM_SERVICE.search(new RealmQuery.Builder().fiql("aLong==42;ctype=~string*").build()); + assertEquals(1, match.getSize()); + assertTrue(match.getResult().stream().anyMatch(r -> r.getName().equals("searchTest3"))); + + match = REALM_SERVICE.search(new RealmQuery.Builder().base("/even").fiql("name==searchTest*;ctype==string*"). + build()); + assertEquals(2, match.getSize()); + assertTrue(match.getResult().stream().anyMatch(r -> r.getName().equals("searchTest2"))); + assertTrue(match.getResult().stream().anyMatch(r -> r.getName().equals("searchTest3"))); + + match = REALM_SERVICE.search(new RealmQuery.Builder().base("/even").fiql("name==searchTest*;ctype=~string*"). + build()); + assertEquals(3, match.getSize()); + assertTrue(match.getResult().stream().anyMatch(r -> r.getName().equals("searchTest2"))); + assertTrue(match.getResult().stream().anyMatch(r -> r.getName().equals("searchTest3"))); + assertTrue(match.getResult().stream().anyMatch(r -> r.getName().equals("searchTest5"))); + + // Negated LIKE + match = REALM_SERVICE.search(new RealmQuery.Builder().fiql("aLong==42;ctype!~string*").build()); + assertEquals(1, match.getSize()); + assertTrue(match.getResult().stream().anyMatch(r -> r.getName().equals("searchTest1"))); + + match = REALM_SERVICE.search(new RealmQuery.Builder().base("/even").fiql("name==searchTest*;ctype=~string5"). + build()); + assertEquals(1, match.getSize()); + assertTrue(match.getResult().stream().anyMatch(r -> r.getName().equals("searchTest5"))); + + match = REALM_SERVICE.search(new RealmQuery.Builder().base("/even").fiql("aLong==4242;ctype==$null").build()); + assertEquals(1, match.getSize()); + assertTrue(match.getResult().stream().anyMatch(r -> r.getName().equals("searchTest4"))); + + // Null / not-null checks on optional plain attrs + match = REALM_SERVICE.search(new RealmQuery.Builder().base("/even").fiql("name==searchTest*;ctype!=$null"). + build()); + assertEquals(3, match.getSize()); + assertTrue(match.getResult().stream().noneMatch(r -> r.getName().equals("searchTest4"))); + + // Numeric ranges + match = REALM_SERVICE.search(new RealmQuery.Builder().base("/even").fiql("name==searchTest*;aLong=gt=42"). + build()); + assertEquals(2, match.getSize()); + assertTrue(match.getResult().stream().anyMatch(r -> r.getName().equals("searchTest2"))); + assertTrue(match.getResult().stream().anyMatch(r -> r.getName().equals("searchTest4"))); + + match = REALM_SERVICE.search(new RealmQuery.Builder().base("/even"). + fiql("name==searchTest*;aLong=ge=80;aLong=le=100").build()); + assertEquals(1, match.getSize()); + assertTrue(match.getResult().stream().anyMatch(r -> r.getName().equals("searchTest2"))); + + match = REALM_SERVICE.search(new RealmQuery.Builder().base("/even").fiql("name==searchTest*;aLong=lt=42"). + build()); + assertEquals(1, match.getSize()); + assertTrue(match.getResult().stream().anyMatch(r -> r.getName().equals("searchTest5"))); + + match = REALM_SERVICE.search(new RealmQuery.Builder().base("/even").fiql("name==searchTest*;aLong=le=42"). + build()); + assertEquals(2, match.getSize()); + assertTrue(match.getResult().stream().anyMatch(r -> r.getName().equals("searchTest3"))); + assertTrue(match.getResult().stream().anyMatch(r -> r.getName().equals("searchTest5"))); + + // Grouping / OR conditions + match = REALM_SERVICE.search(new RealmQuery.Builder().base("/even"). + fiql("name==searchTest*;(aLong==90,ctype==String5)").build()); + assertEquals(2, match.getSize()); + assertTrue(match.getResult().stream().anyMatch(r -> r.getName().equals("searchTest2"))); + assertTrue(match.getResult().stream().anyMatch(r -> r.getName().equals("searchTest5"))); + + // Date equality against multivalue date schema + match = REALM_SERVICE.search(new RealmQuery.Builder().base("/even").fiql("loginDate==2009-05-26").build()); + assertEquals(2, match.getSize()); + assertTrue(match.getResult().stream().anyMatch(r -> r.getName().equals("searchTest2"))); + assertTrue(match.getResult().stream().anyMatch(r -> r.getName().equals("searchTest3"))); + + match = REALM_SERVICE.search(new RealmQuery.Builder().base("/even").fiql("loginDate==2010-05-26").build()); + assertEquals(1, match.getSize()); + assertTrue(match.getResult().stream().anyMatch(r -> r.getName().equals("searchTest3"))); + + // Date range checks on multivalue schema + match = REALM_SERVICE.search(new RealmQuery.Builder().base("/even").fiql("loginDate=gt=2010-01-01").build()); + assertEquals(2, match.getSize()); + assertTrue(match.getResult().stream().anyMatch(r -> r.getName().equals("searchTest3"))); + assertTrue(match.getResult().stream().anyMatch(r -> r.getName().equals("searchTest5"))); + + match = REALM_SERVICE.search(new RealmQuery.Builder().base("/even"). + fiql("loginDate=ge=2010-05-26;loginDate=le=2010-05-26").build()); + assertEquals(1, match.getSize()); + assertTrue(match.getResult().stream().anyMatch(r -> r.getName().equals("searchTest3"))); + + match = REALM_SERVICE.search(new RealmQuery.Builder().base("/even"). + fiql("name==searchTest*;loginDate==$null").build()); + assertEquals(1, match.getSize()); + assertTrue(match.getResult().stream().anyMatch(r -> r.getName().equals("searchTest4"))); + + // Date nullability checks + match = REALM_SERVICE.search(new RealmQuery.Builder().base("/even"). + fiql("name==searchTest*;loginDate!=$null").build()); + assertEquals(3, match.getSize()); + assertTrue(match.getResult().stream().noneMatch(r -> r.getName().equals("searchTest4"))); + + // Validation error for numeric schema with non-numeric value + SyncopeClientException exception = assertThrows(SyncopeClientException.class, + () -> REALM_SERVICE.search(new RealmQuery.Builder().fiql("aLong==notANumber").build())); + assertEquals(ClientExceptionType.InvalidSearchParameters, exception.getType()); } @Test @@ -406,7 +565,7 @@ public void propagate() { @Test public void issueSYNCOPE1472() { // 1. assign twice resource-ldap-orgunit to /odd - RealmTO realmTO = REALM_SERVICE.search(new RealmQuery.Builder().base("/odd").build()).getResult().getFirst(); + RealmTO realmTO = getRealm("/odd").orElseThrow(); realmTO.getResources().clear(); realmTO.getResources().add("resource-ldap-orgunit"); realmTO.getResources().add("resource-ldap-orgunit"); @@ -456,7 +615,7 @@ public void issueSYNCOPE1856() { Response response = REALM_SERVICE.create(SyncopeConstants.ROOT_REALM, realmTO); assertEquals(Response.Status.CREATED.getStatusCode(), response.getStatusInfo().getStatusCode()); childRealm = REALM_SERVICE.search(new RealmQuery.Builder(). - base(SyncopeConstants.ROOT_REALM).keyword("child").build()).getResult().getFirst(); + base(SyncopeConstants.ROOT_REALM).fiql("name=~child").build()).getResult().getFirst(); // MANAGER CANNOT UPDATE /child try { diff --git a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/UserITCase.java b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/UserITCase.java index 5b163f911c0..17d52b7eb9e 100644 --- a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/UserITCase.java +++ b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/UserITCase.java @@ -959,7 +959,7 @@ public void customPolicyRules() { passwordPolicy = createPolicy(PolicyType.PASSWORD, passwordPolicy); assertNotNull(passwordPolicy); - RealmTO realm = REALM_SERVICE.search(new RealmQuery.Builder().keyword("two").build()).getResult().getFirst(); + RealmTO realm = REALM_SERVICE.search(new RealmQuery.Builder().fiql("name==two").build()).getResult().getFirst(); String oldAccountPolicy = realm.getAccountPolicy(); realm.setAccountPolicy(accountPolicy.getKey()); String oldPasswordPolicy = realm.getPasswordPolicy(); diff --git a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/UserIssuesITCase.java b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/UserIssuesITCase.java index 310549c6a53..ab486d23867 100644 --- a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/UserIssuesITCase.java +++ b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/UserIssuesITCase.java @@ -812,7 +812,7 @@ public void issueSYNCOPE420() throws IOException { } assertNotNull(logicActions); - RealmTO realm = REALM_SERVICE.search(new RealmQuery.Builder().keyword("two").build()).getResult().getFirst(); + RealmTO realm = REALM_SERVICE.search(new RealmQuery.Builder().fiql("name==two").build()).getResult().getFirst(); assertNotNull(realm); realm.getActions().add(logicActions.getKey()); REALM_SERVICE.update(realm); @@ -1229,7 +1229,7 @@ public void issueSYNCOPE626() { passwordPolicy = createPolicy(PolicyType.PASSWORD, passwordPolicy); assertNotNull(passwordPolicy); - RealmTO realm = REALM_SERVICE.search(new RealmQuery.Builder().keyword("two").build()).getResult().getFirst(); + RealmTO realm = REALM_SERVICE.search(new RealmQuery.Builder().fiql("name==two").build()).getResult().getFirst(); String oldPasswordPolicy = realm.getPasswordPolicy(); realm.setPasswordPolicy(passwordPolicy.getKey()); REALM_SERVICE.update(realm); @@ -1473,7 +1473,8 @@ public void issueSYNCOPE1337() { PasswordPolicyTO pwdPolicy = POLICY_SERVICE.read(PolicyType.PASSWORD, "ce93fcda-dc3a-4369-a7b0-a6108c261c85"); assertEquals(1, pwdPolicy.getHistoryLength()); - RealmTO evenTwo = REALM_SERVICE.search(new RealmQuery.Builder().keyword("two").build()).getResult().getFirst(); + RealmTO evenTwo = REALM_SERVICE.search(new RealmQuery.Builder().fiql("name==two").build()) + .getResult().getFirst(); evenTwo.setPasswordPolicy(pwdPolicy.getKey()); REALM_SERVICE.update(evenTwo);