This is an automated email from the ASF dual-hosted git repository.

ilgrosso pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/syncope.git


The following commit(s) were added to refs/heads/master by this push:
     new 5a677734d0 [SYNCOPE-1686] Added the End attribute to RelationshipTO to 
indicate the side of the current Any in the relationship. (#924)
5a677734d0 is described below

commit 5a677734d05bd96ae929ab7b1673b0ffdda12f11
Author: Matteo Tatoni <[email protected]>
AuthorDate: Mon Dec 2 13:42:47 2024 +0100

    [SYNCOPE-1686] Added the End attribute to RelationshipTO to indicate the 
side of the current Any in the relationship. (#924)
---
 .../console/panels/RelationshipViewPanel.java      | 210 +++++++++++++++++++++
 .../client/console/wizards/any/Relationships.java  |  90 ++-------
 .../console/panels/RelationshipViewPanel.html      |  58 ++++++
 .../panels/RelationshipViewPanel.properties        |  20 ++
 .../panels/RelationshipViewPanel_it.properties     |  20 ++
 .../syncope/common/lib/to/RelationshipTO.java      |  24 +++
 .../java/data/AbstractAnyDataBinder.java           |  19 +-
 .../java/data/AnyObjectDataBinderImpl.java         |  26 ++-
 .../provisioning/java/data/UserDataBinderImpl.java |  26 ++-
 .../apache/syncope/fit/core/AnyObjectITCase.java   |  76 ++++++++
 10 files changed, 467 insertions(+), 102 deletions(-)

diff --git 
a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/panels/RelationshipViewPanel.java
 
b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/panels/RelationshipViewPanel.java
new file mode 100644
index 0000000000..cbda4895d9
--- /dev/null
+++ 
b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/panels/RelationshipViewPanel.java
@@ -0,0 +1,210 @@
+/*
+ * 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.client.console.panels;
+
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.syncope.client.console.wicket.markup.html.form.ActionLink;
+import 
org.apache.syncope.client.console.wicket.markup.html.form.ActionLinksTogglePanel;
+import org.apache.syncope.client.console.wicket.markup.html.form.ActionsPanel;
+import org.apache.syncope.client.console.wizards.WizardMgtPanel;
+import org.apache.syncope.client.ui.commons.Constants;
+import org.apache.syncope.common.lib.to.AnyObjectTO;
+import org.apache.syncope.common.lib.to.AnyTO;
+import org.apache.syncope.common.lib.to.RelationshipTO;
+import org.apache.syncope.common.lib.to.UserTO;
+import org.apache.syncope.common.lib.types.AnyEntitlement;
+import org.apache.wicket.AttributeModifier;
+import org.apache.wicket.PageReference;
+import org.apache.wicket.ajax.AjaxEventBehavior;
+import org.apache.wicket.ajax.AjaxRequestTarget;
+import org.apache.wicket.markup.html.WebMarkupContainer;
+import org.apache.wicket.markup.html.basic.Label;
+import org.apache.wicket.markup.html.list.ListItem;
+import org.apache.wicket.markup.html.list.ListView;
+import org.apache.wicket.model.Model;
+import org.apache.wicket.model.ResourceModel;
+
+public final class RelationshipViewPanel extends 
WizardMgtPanel<RelationshipTO> {
+
+    private static final long serialVersionUID = -7510529471158257903L;
+
+    private ActionLinksTogglePanel<RelationshipTO> togglePanel;
+
+    private final ListView<RelationshipTO> relationshipsList;
+
+    private RelationshipViewPanel(
+            final String id,
+            final List<RelationshipTO> relationships,
+            final AnyTO anyTO,
+            final boolean reuseItem,
+            final boolean wizardInModal) {
+        super(id, wizardInModal);
+        addInnerObject(getHeader());
+        relationshipsList = new ListView<>("relationships", relationships) {
+
+            private static final long serialVersionUID = 4983556433071042668L;
+
+            @Override
+            protected void populateItem(final ListItem<RelationshipTO> 
relationshipItem) {
+                RelationshipTO relationshipTO = 
relationshipItem.getModelObject();
+                buildRowLabels(relationshipItem, relationshipTO, anyTO);
+
+                ActionsPanel<RelationshipTO> action = new 
ActionsPanel<>("action", new Model<>(relationshipTO));
+                action.add(new ActionLink<>() {
+
+                    private static final long serialVersionUID = 
5207800927605869051L;
+
+                    @Override
+                    public void onClick(final AjaxRequestTarget target, final 
RelationshipTO modelObject) {
+                        relationships.remove(modelObject);
+                        target.add(RelationshipViewPanel.this);
+                    }
+                }, ActionLink.ActionType.DELETE, 
AnyEntitlement.UPDATE.getFor(anyTO.getType()), true).hideLabel();
+
+                if (togglePanel != null) {
+                    relationshipItem.add(new AttributeModifier("style", 
"cursor: pointer;"));
+                    relationshipItem.add(new 
AjaxEventBehavior(Constants.ON_CLICK) {
+
+                        private static final long serialVersionUID = 
-9027652037484739586L;
+
+                        @Override
+                        protected String findIndicatorId() {
+                            return StringUtils.EMPTY;
+                        }
+
+                        @Override
+                        protected void onEvent(final AjaxRequestTarget target) 
{
+                            togglePanel.toggleWithContent(target, action, 
relationshipTO);
+                        }
+                    });
+                }
+
+                if (togglePanel == null) {
+                    relationshipItem.add(action);
+                } else {
+                    relationshipItem.add(new ActionsPanel<>("action", new 
Model<>(relationshipTO))
+                            .setVisible(false)
+                            .setEnabled(false));
+                }
+            }
+        };
+        relationshipsList.setOutputMarkupId(true);
+        relationshipsList.setReuseItems(reuseItem);
+        relationshipsList.setRenderBodyOnly(true);
+
+        addInnerObject(relationshipsList);
+
+    }
+
+    private WebMarkupContainer getHeader() {
+        WebMarkupContainer headerContainer = new WebMarkupContainer("header");
+        headerContainer.add(new Label("header_left_end", 
getString("left.end")));
+        headerContainer.add(new Label("header_relationship", new 
ResourceModel("relationship")));
+        headerContainer.add(new Label("header_right_end", new 
ResourceModel("right.end")));
+        return headerContainer;
+    }
+
+    private void buildRowLabels(
+            final ListItem<RelationshipTO> row,
+            final RelationshipTO relationshipTO,
+            final AnyTO anyTO) {
+        boolean isLeftRelation = relationshipTO.getEnd() == 
RelationshipTO.End.LEFT;
+        String anyName = anyTO instanceof UserTO
+                ? UserTO.class.cast(anyTO).getUsername()
+                : AnyObjectTO.class.cast(anyTO).getName();
+
+        row.add(new Label("relationship", relationshipTO.getType()));
+        Label leftEnd = new Label("left_end", isLeftRelation
+                ? String.format("%s %s", anyTO.getType() , anyName)
+                : String.format("%s %s", relationshipTO.getOtherEndType(), 
relationshipTO.getOtherEndName()));
+
+        Label rightEnd = new Label("right_end", isLeftRelation
+                ? String.format("%s %s", relationshipTO.getOtherEndType(), 
relationshipTO.getOtherEndName())
+                : String.format("%s %s", anyTO.getType() , anyName));
+
+        if (anyTO.getKey() != null && 
anyTO.getKey().equals(relationshipTO.getOtherEndKey())) {
+            setBold(leftEnd, rightEnd);
+        } else {
+            setBold(isLeftRelation ? leftEnd : rightEnd);
+        }
+        row.add(leftEnd, rightEnd);
+    }
+
+    private void setBold(final Label... labels) {
+        for (Label label : labels) {
+            label.add(new AttributeModifier("style", "font-weight: bold;"));
+        }
+    }
+
+    public static class Builder extends WizardMgtPanel.Builder<RelationshipTO> 
{
+
+        private static final long serialVersionUID = -3643771352897992172L;
+
+        private List<RelationshipTO> relationships;
+
+        private AnyTO anyTO;
+
+        private boolean reuseItem = true;
+
+        public Builder(final PageReference pageRef) {
+            super(pageRef);
+            this.relationships = null;
+            this.anyTO = null;
+        }
+
+        public RelationshipViewPanel.Builder setAnyTO(final AnyTO anyTO) {
+            this.anyTO = anyTO;
+            return this;
+        }
+
+
+        public RelationshipViewPanel.Builder setRelationships(final 
List<RelationshipTO> relationships) {
+            this.relationships = relationships;
+            return this;
+        }
+
+        public RelationshipViewPanel.Builder addItem(final RelationshipTO 
item) {
+            if (item == null) {
+                return this;
+            }
+
+            if (this.relationships == null) {
+                this.relationships = new ArrayList<>();
+            }
+
+            this.relationships.add(item);
+            return this;
+        }
+
+        public RelationshipViewPanel.Builder setReuseItem(final boolean 
reuseItem) {
+            this.reuseItem = reuseItem;
+            return this;
+        }
+
+        @Override
+        protected WizardMgtPanel<RelationshipTO> newInstance(final String id, 
final boolean wizardInModal) {
+            return new RelationshipViewPanel(id, relationships, anyTO, 
reuseItem, wizardInModal);
+        }
+
+    }
+
+}
+
diff --git 
a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wizards/any/Relationships.java
 
b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wizards/any/Relationships.java
index b23d29a3cb..385713dd8f 100644
--- 
a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wizards/any/Relationships.java
+++ 
b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wizards/any/Relationships.java
@@ -19,16 +19,13 @@
 package org.apache.syncope.client.console.wizards.any;
 
 import java.util.ArrayList;
-import java.util.HashMap;
 import java.util.List;
-import java.util.Map;
 import java.util.Optional;
 import java.util.stream.Collectors;
 import org.apache.commons.collections4.ListUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.syncope.client.console.panels.AnyDirectoryPanel;
-import org.apache.syncope.client.console.panels.ListViewPanel;
-import org.apache.syncope.client.console.panels.ListViewPanel.ListViewReload;
+import org.apache.syncope.client.console.panels.RelationshipViewPanel;
 import org.apache.syncope.client.console.panels.search.AnyObjectSearchPanel;
 import 
org.apache.syncope.client.console.panels.search.AnyObjectSelectionDirectoryPanel;
 import 
org.apache.syncope.client.console.panels.search.AnySelectionDirectoryPanel;
@@ -47,7 +44,6 @@ import org.apache.syncope.client.ui.commons.Constants;
 import 
org.apache.syncope.client.ui.commons.ajax.form.IndicatorAjaxFormComponentUpdatingBehavior;
 import org.apache.syncope.client.ui.commons.ajax.markup.html.LabelInfo;
 import 
org.apache.syncope.client.ui.commons.markup.html.form.AjaxDropDownChoicePanel;
-import 
org.apache.syncope.client.ui.commons.wicket.markup.html.bootstrap.tabs.Accordion;
 import org.apache.syncope.client.ui.commons.wizards.any.AnyWrapper;
 import org.apache.syncope.client.ui.commons.wizards.any.UserWrapper;
 import org.apache.syncope.common.lib.to.AnyObjectTO;
@@ -61,20 +57,17 @@ import org.apache.syncope.common.lib.types.AnyTypeKind;
 import org.apache.wicket.Component;
 import org.apache.wicket.PageReference;
 import org.apache.wicket.ajax.AjaxRequestTarget;
-import org.apache.wicket.event.Broadcast;
 import org.apache.wicket.event.IEvent;
-import org.apache.wicket.extensions.markup.html.tabs.AbstractTab;
 import org.apache.wicket.extensions.wizard.IWizard;
 import org.apache.wicket.extensions.wizard.WizardModel.ICondition;
 import org.apache.wicket.extensions.wizard.WizardStep;
-import org.apache.wicket.markup.head.IHeaderResponse;
-import org.apache.wicket.markup.head.OnDomReadyHeaderItem;
 import org.apache.wicket.markup.html.WebMarkupContainer;
 import org.apache.wicket.markup.html.basic.Label;
 import org.apache.wicket.markup.html.form.IChoiceRenderer;
 import org.apache.wicket.markup.html.panel.Fragment;
 import org.apache.wicket.markup.html.panel.Panel;
 import org.apache.wicket.model.IModel;
+import org.apache.wicket.model.Model;
 import org.apache.wicket.model.PropertyModel;
 import org.apache.wicket.model.ResourceModel;
 import org.apache.wicket.model.util.ListModel;
@@ -133,47 +126,16 @@ public class Relationships extends WizardStep implements 
ICondition {
     }
 
     protected Fragment getViewFragment() {
-        Map<String, List<RelationshipTO>> relationships = new HashMap<>();
-        addRelationship(relationships, 
getCurrentRelationships().toArray(RelationshipTO[]::new));
-
         Fragment viewFragment = new Fragment("relationships", "viewFragment", 
this);
         viewFragment.setOutputMarkupId(true);
 
-        viewFragment.add(new Accordion("relationships", 
relationships.keySet().stream().
-                map(relationship -> new AbstractTab(new 
ResourceModel("relationship", relationship)) {
-
-            private static final long serialVersionUID = 1037272333056449378L;
-
-            @Override
-            public Panel getPanel(final String panelId) {
-                return new ListViewPanel.Builder<>(RelationshipTO.class, 
pageRef).
-                        setItems(relationships.get(relationship)).
-                        includes("otherEndType", "otherEndKey", 
"otherEndName").
-                        addAction(new ActionLink<>() {
-
-                            private static final long serialVersionUID = 
-6847033126124401556L;
-
-                            @Override
-                            public void onClick(final AjaxRequestTarget 
target, final RelationshipTO modelObject) {
-                                removeRelationships(relationships, 
modelObject);
-                                send(Relationships.this, Broadcast.DEPTH, new 
ListViewReload<>(target));
-                            }
-                        }, ActionType.DELETE, 
AnyEntitlement.UPDATE.getFor(anyTO.getType()), true).
-                        build(panelId);
-            }
-        }).collect(Collectors.toList())) {
-
-            private static final long serialVersionUID = 1037272333056449379L;
-
-            @Override
-            public void renderHead(final IHeaderResponse response) {
-                super.renderHead(response);
-                if (relationships.isEmpty()) {
-                    
response.render(OnDomReadyHeaderItem.forScript(String.format(
-                            "$('#emptyPlaceholder').append(\"%s\")", 
getString("relationships.empty.list"))));
-                }
-            }
-        });
+        List<RelationshipTO> relationships = getCurrentRelationships();
+        viewFragment.add(relationships.isEmpty()
+                ? new Label("relationships", new 
Model<>(getString("relationships.empty.list")))
+                : new RelationshipViewPanel.Builder(pageRef).
+                        setAnyTO(anyTO).
+                        setRelationships(relationships).
+                        build("relationships"));
 
         ActionsPanel<RelationshipTO> panel = new ActionsPanel<>("actions", 
null);
         viewFragment.add(panel);
@@ -200,42 +162,10 @@ public class Relationships extends WizardStep implements 
ICondition {
                 : List.of();
     }
 
-    protected void addRelationship(
-            final Map<String, List<RelationshipTO>> relationships,
-            final RelationshipTO... rels) {
-
-        for (RelationshipTO relationship : rels) {
-            List<RelationshipTO> listrels;
-            if (relationships.containsKey(relationship.getType())) {
-                listrels = relationships.get(relationship.getType());
-            } else {
-                listrels = new ArrayList<>();
-                relationships.put(relationship.getType(), listrels);
-            }
-            listrels.add(relationship);
-        }
-    }
-
     protected void addNewRelationships(final RelationshipTO... rels) {
         getCurrentRelationships().addAll(List.of(rels));
     }
 
-    protected void removeRelationships(
-            final Map<String, List<RelationshipTO>> relationships, final 
RelationshipTO... rels) {
-
-        List<RelationshipTO> currentRels = getCurrentRelationships();
-        for (RelationshipTO relationship : rels) {
-            currentRels.remove(relationship);
-            if (relationships.containsKey(relationship.getType())) {
-                List<RelationshipTO> rellist = 
relationships.get(relationship.getType());
-                rellist.remove(relationship);
-                if (rellist.isEmpty()) {
-                    relationships.remove(relationship.getType());
-                }
-            }
-        }
-    }
-
     @Override
     public boolean evaluate() {
         // [SYNCOPE-1171] - skip current step when the are no relationships 
types in Syncope
@@ -265,6 +195,7 @@ public class Relationships extends WizardStep implements 
ICondition {
         public Specification() {
             super("specification");
             rel = new RelationshipTO();
+            rel.setEnd(RelationshipTO.End.LEFT);
 
             List<String> availableRels = 
relationshipTypeRestClient.list().stream().
                     
map(RelationshipTypeTO::getKey).collect(Collectors.toList());
@@ -396,6 +327,7 @@ public class Relationships extends WizardStep implements 
ICondition {
 
                 AnyTO right = 
AnySelectionDirectoryPanel.ItemSelection.class.cast(event.getPayload()).getSelection();
                 rel.setOtherEndKey(right.getKey());
+                rel.setOtherEndName(AnyObjectTO.class.cast(right).getName());
 
                 Relationships.this.addNewRelationships(rel);
 
diff --git 
a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/panels/RelationshipViewPanel.html
 
b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/panels/RelationshipViewPanel.html
new file mode 100644
index 0000000000..7ca16e54db
--- /dev/null
+++ 
b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/panels/RelationshipViewPanel.html
@@ -0,0 +1,58 @@
+<!--
+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.
+-->
+
+<html xmlns="http://www.w3.org/1999/xhtml"; 
xmlns:wicket="http://wicket.apache.org";>
+  <head>
+    <title>Relationship view panel</title>
+  </head>
+  <body>
+    <wicket:extend>
+      <div class="col-xs-12">
+        <div class="card-body table-responsive no-padding">
+          <table class="table table-hover">
+            <tbody>
+              <tr wicket:id="header">
+                <th style="text-align: center"><span 
wicket:id="header_left_end"/></th>
+                <th style="text-align: center"><span 
wicket:id="header_relationship"/></th>
+                <th style="text-align: center"><span 
wicket:id="header_right_end"/></th>
+                <th style="text-align: center" />
+              </tr>
+              <tr wicket:id="relationships">
+                <td class="col_width list_view_panel_labels" 
style="text-align: center">
+                  <span wicket:id="left_end"/>
+                </td>
+                <td class="col_width list_view_panel_labels" 
style="text-align: center">
+                  <span wicket:id="relationship"/>
+                </td>
+                <td class="col_width list_view_panel_labels" 
style="text-align: center">
+                  <span wicket:id="right_end"/>
+                </td>
+                <td>
+                  <div class="listview-actions">
+                    <span wicket:id="action">[actions]</span>
+                  </div>
+                </td>
+              </tr>
+            </tbody>
+          </table>
+        </div>
+      </div>
+    </wicket:extend>
+  </body>
+</html>
diff --git 
a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/panels/RelationshipViewPanel.properties
 
b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/panels/RelationshipViewPanel.properties
new file mode 100644
index 0000000000..b9928586d9
--- /dev/null
+++ 
b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/panels/RelationshipViewPanel.properties
@@ -0,0 +1,20 @@
+# 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.
+
+left.end=Left End
+relationship=Relationship
+right.end=Right End
diff --git 
a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/panels/RelationshipViewPanel_it.properties
 
b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/panels/RelationshipViewPanel_it.properties
new file mode 100644
index 0000000000..d2d1f89876
--- /dev/null
+++ 
b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/panels/RelationshipViewPanel_it.properties
@@ -0,0 +1,20 @@
+# 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.
+
+left.end=Lato Sinistro
+relationship=Relazione
+right.end=Lato Destro
diff --git 
a/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/to/RelationshipTO.java
 
b/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/to/RelationshipTO.java
index 8100da88d0..4222a24611 100644
--- 
a/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/to/RelationshipTO.java
+++ 
b/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/to/RelationshipTO.java
@@ -26,12 +26,24 @@ public class RelationshipTO implements BaseBean {
 
     private static final long serialVersionUID = 360672942026613929L;
 
+    public enum End {
+        LEFT,
+        RIGHT;
+
+    }
+
     public static class Builder {
 
         private final RelationshipTO instance = new RelationshipTO();
 
         public Builder(final String type) {
             instance.setType(type);
+            instance.setEnd(End.LEFT);
+        }
+
+        public Builder(final String type, final End end) {
+            instance.setType(type);
+            instance.setEnd(end);
         }
 
         public Builder otherEnd(final String otherEndType, final String 
otherEndKey) {
@@ -54,6 +66,8 @@ public class RelationshipTO implements BaseBean {
 
     private String type;
 
+    private End end;
+
     private String otherEndType;
 
     private String otherEndKey;
@@ -92,6 +106,14 @@ public class RelationshipTO implements BaseBean {
         this.otherEndName = otherEndName;
     }
 
+    public End getEnd() {
+        return end;
+    }
+
+    public void setEnd(final End end) {
+        this.end = end;
+    }
+
     @Override
     public int hashCode() {
         return new HashCodeBuilder().
@@ -99,6 +121,7 @@ public class RelationshipTO implements BaseBean {
                 append(otherEndType).
                 append(otherEndKey).
                 append(otherEndName).
+                append(end).
                 build();
     }
 
@@ -119,6 +142,7 @@ public class RelationshipTO implements BaseBean {
                 append(otherEndType, other.otherEndType).
                 append(otherEndKey, other.otherEndKey).
                 append(otherEndName, other.otherEndName).
+                append(end, other.end).
                 build();
     }
 }
diff --git 
a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/AbstractAnyDataBinder.java
 
b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/AbstractAnyDataBinder.java
index 46c66ecb2a..392fed66b4 100644
--- 
a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/AbstractAnyDataBinder.java
+++ 
b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/AbstractAnyDataBinder.java
@@ -75,6 +75,7 @@ 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.VirSchema;
 import org.apache.syncope.core.persistence.api.entity.anyobject.AnyObject;
+import org.apache.syncope.core.persistence.api.entity.user.User;
 import org.apache.syncope.core.provisioning.api.AccountGetter;
 import org.apache.syncope.core.provisioning.api.DerAttrHandler;
 import org.apache.syncope.core.provisioning.api.IntAttrName;
@@ -125,9 +126,17 @@ abstract class AbstractAnyDataBinder {
         
anyTO.getResources().addAll(resources.stream().map(ExternalResource::getKey).collect(Collectors.toSet()));
     }
 
-    protected static RelationshipTO getRelationshipTO(final String 
relationshipType, final AnyObject otherEnd) {
-        return new RelationshipTO.Builder(relationshipType).
-                otherEnd(otherEnd.getType().getKey(), otherEnd.getKey(), 
otherEnd.getName()).
+    protected static RelationshipTO getRelationshipTO(
+            final String relationshipType,
+            final RelationshipTO.End end,
+            final Any<?> otherEnd) {
+
+        return new RelationshipTO.Builder(relationshipType, end).otherEnd(
+                otherEnd.getType().getKey(),
+                otherEnd.getKey(),
+                otherEnd instanceof User user
+                        ? user.getUsername()
+                        : ((AnyObject) otherEnd).getName()).
                 build();
     }
 
@@ -518,7 +527,7 @@ abstract class AbstractAnyDataBinder {
                         }
                     },
                     () -> LOG.debug("Invalid {} {}, ignoring...",
-                        AnyTypeClass.class.getSimpleName(), patch.getValue()));
+                            AnyTypeClass.class.getSimpleName(), 
patch.getValue()));
         }
 
         // 2. resources
@@ -536,7 +545,7 @@ abstract class AbstractAnyDataBinder {
                         }
                     },
                     () -> LOG.debug("Invalid {} {}, ignoring...",
-                        ExternalResource.class.getSimpleName(), 
patch.getValue()));
+                            ExternalResource.class.getSimpleName(), 
patch.getValue()));
         }
 
         Set<ExternalResource> resources = anyUtils.getAllResources(any);
diff --git 
a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/AnyObjectDataBinderImpl.java
 
b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/AnyObjectDataBinderImpl.java
index 4749ac04a6..a6b719905a 100644
--- 
a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/AnyObjectDataBinderImpl.java
+++ 
b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/AnyObjectDataBinderImpl.java
@@ -36,6 +36,7 @@ import org.apache.syncope.common.lib.request.AttrPatch;
 import org.apache.syncope.common.lib.to.AnyObjectTO;
 import org.apache.syncope.common.lib.to.ConnObject;
 import org.apache.syncope.common.lib.to.MembershipTO;
+import org.apache.syncope.common.lib.to.RelationshipTO;
 import org.apache.syncope.common.lib.types.AnyTypeKind;
 import org.apache.syncope.common.lib.types.ClientExceptionType;
 import org.apache.syncope.common.lib.types.PatchOperation;
@@ -160,8 +161,11 @@ public class AnyObjectDataBinderImpl extends 
AbstractAnyDataBinder implements An
                             map(relationship -> getRelationshipTO(
                             relationship.getType().getKey(),
                             
relationship.getLeftEnd().getKey().equals(anyObject.getKey())
+                            ? RelationshipTO.End.LEFT
+                            : RelationshipTO.End.RIGHT,
+                            
relationship.getLeftEnd().getKey().equals(anyObject.getKey())
                             ? relationship.getRightEnd()
-                            : anyObject)).
+                            : relationship.getLeftEnd())).
                             toList());
 
             // memberships
@@ -232,10 +236,13 @@ public class AnyObjectDataBinderImpl extends 
AbstractAnyDataBinder implements An
                 AnyObject otherEnd = 
anyObjectDAO.findById(relationshipTO.getOtherEndKey()).orElse(null);
                 if (otherEnd == null) {
                     LOG.debug("Ignoring invalid anyObject {}", 
relationshipTO.getOtherEndKey());
+                } else if (relationshipTO.getEnd() == 
RelationshipTO.End.RIGHT) {
+                    SyncopeClientException noRight =
+                            
SyncopeClientException.build(ClientExceptionType.InvalidRelationship);
+                    noRight.getElements().add(
+                            "Relationships shall be created or updated only 
from their left end");
+                    scce.addException(noRight);
                 } else if (relationships.contains(Pair.of(otherEnd.getKey(), 
relationshipTO.getType()))) {
-                    LOG.error("{} was already in relationship {} with {}",
-                            otherEnd, relationshipTO.getType(), anyObject);
-
                     SyncopeClientException assigned =
                             
SyncopeClientException.build(ClientExceptionType.InvalidRelationship);
                     assigned.getElements().add("AnyObject was already in 
relationship "
@@ -357,12 +364,15 @@ public class AnyObjectDataBinderImpl extends 
AbstractAnyDataBinder implements An
                                 orElse(null);
                         if (otherEnd == null) {
                             LOG.debug("Ignoring invalid any object {}", 
patch.getRelationshipTO().getOtherEndKey());
+                        } else if (patch.getRelationshipTO().getEnd() == 
RelationshipTO.End.RIGHT) {
+                            SyncopeClientException noRight =
+                                    
SyncopeClientException.build(ClientExceptionType.InvalidRelationship);
+                            noRight.getElements().add(
+                                    "Relationships shall be created or updated 
only from their left end");
+                            scce.addException(noRight);
                         } else if (relationships.contains(
                                 Pair.of(otherEnd.getKey(), 
patch.getRelationshipTO().getType()))) {
 
-                            LOG.error("{} was already in relationship {} with 
{}",
-                                    anyObject, 
patch.getRelationshipTO().getType(), otherEnd);
-
                             SyncopeClientException assigned =
                                     
SyncopeClientException.build(ClientExceptionType.InvalidRelationship);
                             assigned.getElements().add("AnyObject was already 
in relationship "
@@ -431,7 +441,7 @@ public class AnyObjectDataBinderImpl extends 
AbstractAnyDataBinder implements An
                         PlainSchema schema = 
getPlainSchema(attrTO.getSchema());
                         if (schema == null) {
                             LOG.debug("Invalid {}{}, ignoring...",
-                                PlainSchema.class.getSimpleName(), 
attrTO.getSchema());
+                                    PlainSchema.class.getSimpleName(), 
attrTO.getSchema());
                         } else {
                             Optional<? extends APlainAttr> attr =
                                     anyObject.getPlainAttr(schema.getKey(), 
newMembership);
diff --git 
a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/UserDataBinderImpl.java
 
b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/UserDataBinderImpl.java
index cfd2e6984b..f42c994f1f 100644
--- 
a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/UserDataBinderImpl.java
+++ 
b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/UserDataBinderImpl.java
@@ -44,6 +44,7 @@ import org.apache.syncope.common.lib.to.ConnObject;
 import org.apache.syncope.common.lib.to.Item;
 import org.apache.syncope.common.lib.to.LinkedAccountTO;
 import org.apache.syncope.common.lib.to.MembershipTO;
+import org.apache.syncope.common.lib.to.RelationshipTO;
 import org.apache.syncope.common.lib.to.UserTO;
 import org.apache.syncope.common.lib.types.AnyTypeKind;
 import org.apache.syncope.common.lib.types.CipherAlgorithm;
@@ -347,9 +348,13 @@ public class UserDataBinderImpl extends 
AbstractAnyDataBinder implements UserDat
             AnyObject otherEnd = 
anyObjectDAO.findById(relationshipTO.getOtherEndKey()).orElse(null);
             if (otherEnd == null) {
                 LOG.debug("Ignoring invalid anyObject {}", 
relationshipTO.getOtherEndKey());
+            } else if (relationshipTO.getEnd() == RelationshipTO.End.RIGHT) {
+                SyncopeClientException noRight =
+                        
SyncopeClientException.build(ClientExceptionType.InvalidRelationship);
+                noRight.getElements().add(
+                        "Relationships shall be created or updated only from 
their left end");
+                scce.addException(noRight);
             } else if (relationships.contains(Pair.of(otherEnd.getKey(), 
relationshipTO.getType()))) {
-                LOG.error("{} was already in relationship {} with {}", 
otherEnd, relationshipTO.getType(), user);
-
                 SyncopeClientException assigned =
                         
SyncopeClientException.build(ClientExceptionType.InvalidRelationship);
                 assigned.getElements().add(otherEnd.getType().getKey() + " " + 
otherEnd.getName()
@@ -369,8 +374,6 @@ public class UserDataBinderImpl extends 
AbstractAnyDataBinder implements UserDat
                         },
                         () -> LOG.debug("Ignoring invalid relationship type 
{}", relationshipTO.getType()));
             } else {
-                LOG.error("{} cannot be related to {}", otherEnd, user);
-
                 SyncopeClientException unrelatable =
                         
SyncopeClientException.build(ClientExceptionType.InvalidRelationship);
                 unrelatable.getElements().add(otherEnd.getType().getKey() + " 
" + otherEnd.getName()
@@ -550,15 +553,18 @@ public class UserDataBinderImpl extends 
AbstractAnyDataBinder implements UserDat
                     } else if (relationships.contains(
                             Pair.of(otherEnd.getKey(), 
patch.getRelationshipTO().getType()))) {
 
-                        LOG.error("{} was already in relationship {} with {}",
-                                user, patch.getRelationshipTO().getType(), 
otherEnd);
-
                         SyncopeClientException assigned =
                                 
SyncopeClientException.build(ClientExceptionType.InvalidRelationship);
                         assigned.getElements().add("User was already in 
relationship "
                                 + patch.getRelationshipTO().getType() + " with 
"
                                 + otherEnd.getType().getKey() + " " + 
otherEnd.getName());
                         scce.addException(assigned);
+                    } else if (patch.getRelationshipTO().getEnd() == 
RelationshipTO.End.RIGHT) {
+                        SyncopeClientException noRight =
+                                
SyncopeClientException.build(ClientExceptionType.InvalidRelationship);
+                        noRight.getElements().add(
+                                "Relationships shall be created or updated 
only from their left end");
+                        scce.addException(noRight);
                     } else if 
(user.getRealm().getFullPath().startsWith(otherEnd.getRealm().getFullPath())) {
                         relationships.add(Pair.of(otherEnd.getKey(), 
patch.getRelationshipTO().getType()));
 
@@ -628,7 +634,7 @@ public class UserDataBinderImpl extends 
AbstractAnyDataBinder implements UserDat
                         PlainSchema schema = 
getPlainSchema(attrTO.getSchema());
                         if (schema == null) {
                             LOG.debug("Invalid {}{}, ignoring...",
-                                PlainSchema.class.getSimpleName(), 
attrTO.getSchema());
+                                    PlainSchema.class.getSimpleName(), 
attrTO.getSchema());
                         } else {
                             UPlainAttr attr = 
user.getPlainAttr(schema.getKey(), newMembership).orElse(null);
                             if (attr == null) {
@@ -820,8 +826,8 @@ public class UserDataBinderImpl extends 
AbstractAnyDataBinder implements UserDat
                     flatMap(role -> 
role.getPrivileges().stream()).map(Privilege::getKey).collect(Collectors.toSet()));
 
             // relationships
-            userTO.getRelationships().addAll(user.getRelationships().stream().
-                    map(relationship -> 
getRelationshipTO(relationship.getType().getKey(), relationship.getRightEnd())).
+            
userTO.getRelationships().addAll(user.getRelationships().stream().map(relationship
 -> getRelationshipTO(
+                    relationship.getType().getKey(), RelationshipTO.End.LEFT, 
relationship.getRightEnd())).
                     toList());
 
             // memberships
diff --git 
a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/AnyObjectITCase.java
 
b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/AnyObjectITCase.java
index d91bfdc52d..1f392cb763 100644
--- 
a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/AnyObjectITCase.java
+++ 
b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/AnyObjectITCase.java
@@ -21,6 +21,7 @@ package org.apache.syncope.fit.core;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.junit.jupiter.api.Assertions.fail;
 
@@ -253,4 +254,79 @@ public class AnyObjectITCase extends AbstractITCase {
         assertFalse(printer.getResources().contains(RESOURCE_NAME_DBSCRIPTED), 
"Should not contain removed resources");
         assertFalse(printer.getAuxClasses().contains("csv"), "Should not 
contain removed auxiliary classes");
     }
+
+    @Test
+    public void issueSYNCOPE1686() {
+        // Create printers
+        AnyObjectCR printer1CR = getSample("printer1");
+        printer1CR.getResources().clear();
+        String key1 = createAnyObject(printer1CR).getEntity().getKey();
+
+        AnyObjectCR printer2CR = getSample("printer2");
+        printer2CR.getResources().clear();
+        String key2 = createAnyObject(printer2CR).getEntity().getKey();
+
+        AnyObjectCR printer3CR = getSample("printer3");
+        printer3CR.getResources().clear();
+        String key3 = createAnyObject(printer3CR).getEntity().getKey();
+
+        // Add relationships: printer1 -> printer2 and printer2 -> printer3
+        AnyObjectUR relationship1To2 = new AnyObjectUR.Builder(key1)
+                .relationship(new RelationshipUR.Builder(
+                        new 
RelationshipTO.Builder("neighborhood").otherEnd(PRINTER, key2).build()).build())
+                .build();
+        AnyObjectUR relationship2To3 = new AnyObjectUR.Builder(key2)
+                .relationship(new RelationshipUR.Builder(
+                        new 
RelationshipTO.Builder("neighborhood").otherEnd(PRINTER, key3).build()).build())
+                .build();
+
+        updateAnyObject(relationship1To2);
+        updateAnyObject(relationship2To3);
+
+        // Read updated printers
+        AnyObjectTO printer1 = ANY_OBJECT_SERVICE.read(key1);
+        AnyObjectTO printer2 = ANY_OBJECT_SERVICE.read(key2);
+        AnyObjectTO printer3 = ANY_OBJECT_SERVICE.read(key3);
+
+        // Verify relationships for printer1
+        assertEquals(1, printer1.getRelationships().size());
+        RelationshipTO rel1 = printer1.getRelationships().get(0);
+        assertEquals(RelationshipTO.End.LEFT, rel1.getEnd());
+        assertEquals(printer2.getKey(), rel1.getOtherEndKey());
+        assertEquals(printer2.getType(), rel1.getOtherEndType());
+        assertEquals(printer2.getName(), rel1.getOtherEndName());
+
+        // Verify relationships for printer2
+        assertEquals(2, printer2.getRelationships().size());
+        assertTrue(printer2.getRelationships().stream()
+                .anyMatch(r -> r.getEnd() == RelationshipTO.End.LEFT
+                && printer3.getKey().equals(r.getOtherEndKey())
+                && printer3.getType().equals(r.getOtherEndType())
+                && printer3.getName().equals(r.getOtherEndName())));
+        assertTrue(printer2.getRelationships().stream()
+                .anyMatch(r -> r.getEnd() == RelationshipTO.End.RIGHT
+                && printer1.getKey().equals(r.getOtherEndKey())
+                && printer1.getType().equals(r.getOtherEndType())
+                && printer1.getName().equals(r.getOtherEndName())));
+
+        // Verify relationships for printer3
+        assertEquals(1, printer3.getRelationships().size());
+        RelationshipTO rel3 = printer3.getRelationships().get(0);
+        assertEquals(RelationshipTO.End.RIGHT, rel3.getEnd());
+        assertEquals(printer2.getKey(), rel3.getOtherEndKey());
+        assertEquals(printer2.getType(), rel3.getOtherEndType());
+        assertEquals(printer2.getName(), rel3.getOtherEndName());
+
+        // Test invalid relationship with End.RIGHT
+        AnyObjectCR printer4CR = getSample("printer4");
+        printer4CR.getResources().clear();
+        printer4CR.getRelationships().add(
+                new RelationshipTO.Builder("neighborhood", 
RelationshipTO.End.RIGHT).otherEnd(PRINTER, key1).build());
+
+        SyncopeClientException exception =
+                assertThrows(SyncopeClientException.class, () -> 
createAnyObject(printer4CR));
+        assertEquals(ClientExceptionType.InvalidRelationship, 
exception.getType());
+        assertTrue(exception.getMessage().
+                contains("Relationships shall be created or updated only from 
their left end"));
+    }
 }


Reply via email to