001/*
002 * Copyright 2015-2024 Ping Identity Corporation
003 *
004 * This program is free software; you can redistribute it and/or modify
005 * it under the terms of the GNU General Public License (GPLv2 only)
006 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
007 * as published by the Free Software Foundation.
008 *
009 * This program is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
012 * GNU General Public License for more details.
013 *
014 * You should have received a copy of the GNU General Public License
015 * along with this program; if not, see <http://www.gnu.org/licenses>.
016 */
017
018package com.unboundid.scim2.server.utils;
019
020import com.fasterxml.jackson.databind.JsonNode;
021import com.fasterxml.jackson.databind.node.ObjectNode;
022import com.unboundid.scim2.common.Path;
023import com.unboundid.scim2.common.ScimResource;
024import com.unboundid.scim2.common.annotations.NotNull;
025import com.unboundid.scim2.common.annotations.Nullable;
026import com.unboundid.scim2.common.exceptions.ScimException;
027import com.unboundid.scim2.common.messages.SortOrder;
028import com.unboundid.scim2.common.types.AttributeDefinition;
029import com.unboundid.scim2.common.utils.Debug;
030import com.unboundid.scim2.common.utils.JsonUtils;
031
032import java.util.Comparator;
033import java.util.Iterator;
034import java.util.List;
035
036/**
037 * A comparator implementation that could be used to compare POJOs representing
038 * SCIM resources using the SCIM sorting parameters.
039 */
040public class ResourceComparator<T extends ScimResource>
041    implements Comparator<T>
042{
043  @NotNull
044  private final Path sortBy;
045
046  @NotNull
047  private final SortOrder sortOrder;
048
049  @Nullable
050  private final ResourceTypeDefinition resourceType;
051
052  /**
053   * Create a new ScimComparator that will sort in ascending order.
054   *
055   * @param sortBy The path to the attribute to sort by.
056   * @param resourceType The resource type definition containing the schemas or
057   *                     {@code null} to compare using case insensitive matching
058   *                     for string values.
059   */
060  public ResourceComparator(@NotNull final Path sortBy,
061                            @Nullable final ResourceTypeDefinition resourceType)
062  {
063    this(sortBy, SortOrder.ASCENDING, resourceType);
064  }
065
066  /**
067   * Create a new ScimComparator.
068   *
069   * @param sortBy The path to the attribute to sort by.
070   * @param sortOrder The sort order.
071   * @param resourceType The resource type definition containing the schemas or
072   *                     {@code null} to compare using case insensitive matching
073   *                     for string values.
074   */
075  public ResourceComparator(@NotNull final Path sortBy,
076                            @Nullable final SortOrder sortOrder,
077                            @Nullable final ResourceTypeDefinition resourceType)
078  {
079    this.sortBy = sortBy;
080    this.sortOrder = sortOrder == null ? SortOrder.ASCENDING : sortOrder;
081    this.resourceType = resourceType;
082  }
083
084  /**
085   * {@inheritDoc}
086   */
087  public int compare(@NotNull final T o1, @NotNull final T o2)
088  {
089    ObjectNode n1 = o1.asGenericScimResource().getObjectNode();
090    ObjectNode n2 = o2.asGenericScimResource().getObjectNode();
091
092    JsonNode v1 = null;
093    JsonNode v2 = null;
094
095    try
096    {
097      List<JsonNode> v1s = JsonUtils.findMatchingPaths(sortBy, n1);
098      if(!v1s.isEmpty())
099      {
100        // Always just use the primary or first value of the first found node.
101        v1 = getPrimaryOrFirst(v1s.get(0));
102      }
103    }
104    catch (ScimException e)
105    {
106      Debug.debugException(e);
107    }
108
109    try
110    {
111      List<JsonNode> v2s = JsonUtils.findMatchingPaths(sortBy, n2);
112      if(!v2s.isEmpty())
113      {
114        // Always just use the primary or first value of the first found node.
115        v2 = getPrimaryOrFirst(v2s.get(0));
116      }
117    }
118    catch (ScimException e)
119    {
120      Debug.debugException(e);
121    }
122
123    if(v1 == null && v2 == null)
124    {
125      return 0;
126    }
127    // or all attribute types, if there is no data for the specified "sortBy"
128    // value they are sorted via the "sortOrder" parameter; i.e., they are
129    // ordered last if ascending and first if descending.
130    else if(v1 == null)
131    {
132      return sortOrder == SortOrder.ASCENDING ? 1 : -1;
133    }
134    else if(v2 == null)
135    {
136      return sortOrder == SortOrder.ASCENDING ? -1 : 1;
137    }
138    else
139    {
140      AttributeDefinition attributeDefinition =
141          resourceType == null ? null :
142              resourceType.getAttributeDefinition(sortBy);
143      return sortOrder == SortOrder.ASCENDING ?
144          JsonUtils.compareTo(v1, v2, attributeDefinition) :
145          JsonUtils.compareTo(v2, v1, attributeDefinition);
146    }
147  }
148
149  /**
150   * Retrieve the value of a complex multi-valued attribute that is marked as
151   * primary or the first value in the list. If the provided node is not an
152   * array node, then just return the provided node.
153   *
154   * @param node The JsonNode to retrieve from.
155   * @return The primary or first value or {@code null} if the provided array
156   * node is empty.
157   */
158  @Nullable
159  private JsonNode getPrimaryOrFirst(@NotNull final JsonNode node)
160  {
161    // if it's a multi-valued attribute (see Section 2.4
162    // [I-D.ietf - scim - core - schema]), if any, or else the first value in
163    // the list, if any.
164
165    if(!node.isArray())
166    {
167      return node;
168    }
169
170    if(node.size() == 0)
171    {
172      return null;
173    }
174
175    Iterator<JsonNode> i = node.elements();
176    while(i.hasNext())
177    {
178      JsonNode value = i.next();
179      JsonNode primary = value.get("primary");
180      if(primary != null && primary.booleanValue())
181      {
182        return value;
183      }
184    }
185    return node.get(0);
186  }
187}