001/*
002 * Copyright 2015-2018 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.exceptions.ScimException;
025import com.unboundid.scim2.common.messages.SortOrder;
026import com.unboundid.scim2.common.types.AttributeDefinition;
027import com.unboundid.scim2.common.utils.Debug;
028import com.unboundid.scim2.common.utils.JsonUtils;
029
030import java.util.Comparator;
031import java.util.Iterator;
032import java.util.List;
033
034/**
035 * A comparator implementation that could be used to compare POJOs representing
036 * SCIM resources using the SCIM sorting parameters.
037 */
038public class ResourceComparator<T extends ScimResource>
039    implements Comparator<T>
040{
041  private final Path sortBy;
042  private final SortOrder sortOrder;
043  private final ResourceTypeDefinition resourceType;
044
045  /**
046   * Create a new ScimComparator that will sort in ascending order.
047   *
048   * @param sortBy The path to the attribute to sort by.
049   * @param resourceType The resource type definition containing the schemas or
050   *                     {@code null} to compare using case insensitive matching
051   *                     for string values.
052   */
053  public ResourceComparator(final Path sortBy,
054                            final ResourceTypeDefinition resourceType)
055  {
056    this(sortBy, SortOrder.ASCENDING, resourceType);
057  }
058
059  /**
060   * Create a new ScimComparator.
061   *
062   * @param sortBy The path to the attribute to sort by.
063   * @param sortOrder The sort order.
064   * @param resourceType The resource type definition containing the schemas or
065   *                     {@code null} to compare using case insensitive matching
066   *                     for string values.
067   */
068  public ResourceComparator(final Path sortBy, final SortOrder sortOrder,
069                            final ResourceTypeDefinition resourceType)
070  {
071    this.sortBy = sortBy;
072    this.sortOrder = sortOrder == null ? SortOrder.ASCENDING : sortOrder;
073    this.resourceType = resourceType;
074  }
075
076  /**
077   * {@inheritDoc}
078   */
079  public int compare(final T o1, final T o2)
080  {
081    ObjectNode n1 = o1.asGenericScimResource().getObjectNode();
082    ObjectNode n2 = o2.asGenericScimResource().getObjectNode();
083
084    JsonNode v1 = null;
085    JsonNode v2 = null;
086
087    try
088    {
089      List<JsonNode> v1s = JsonUtils.findMatchingPaths(sortBy, n1);
090      if(!v1s.isEmpty())
091      {
092        // Always just use the primary or first value of the first found node.
093        v1 = getPrimaryOrFirst(v1s.get(0));
094      }
095    }
096    catch (ScimException e)
097    {
098      Debug.debugException(e);
099    }
100
101    try
102    {
103      List<JsonNode> v2s = JsonUtils.findMatchingPaths(sortBy, n2);
104      if(!v2s.isEmpty())
105      {
106        // Always just use the primary or first value of the first found node.
107        v2 = getPrimaryOrFirst(v2s.get(0));
108      }
109    }
110    catch (ScimException e)
111    {
112      Debug.debugException(e);
113    }
114
115    if(v1 == null && v2 == null)
116    {
117      return 0;
118    }
119    // or all attribute types, if there is no data for the specified "sortBy"
120    // value they are sorted via the "sortOrder" parameter; i.e., they are
121    // ordered last if ascending and first if descending.
122    else if(v1 == null)
123    {
124      return sortOrder == SortOrder.ASCENDING ? 1 : -1;
125    }
126    else if(v2 == null)
127    {
128      return sortOrder == SortOrder.ASCENDING ? -1 : 1;
129    }
130    else
131    {
132      AttributeDefinition attributeDefinition =
133          resourceType == null ? null :
134              resourceType.getAttributeDefinition(sortBy);
135      return sortOrder == SortOrder.ASCENDING ?
136          JsonUtils.compareTo(v1, v2, attributeDefinition) :
137          JsonUtils.compareTo(v2, v1, attributeDefinition);
138    }
139  }
140
141  /**
142   * Retrieve the value of a complex multi-valued attribute that is marked as
143   * primary or the first value in the list. If the provided node is not an
144   * array node, then just return the provided node.
145   *
146   * @param node The JsonNode to retrieve from.
147   * @return The primary or first value or {@code null} if the provided array
148   * node is empty.
149   */
150  private JsonNode getPrimaryOrFirst(final JsonNode node)
151  {
152    // if it's a multi-valued attribute (see Section 2.4
153    // [I-D.ietf - scim - core - schema]), if any, or else the first value in
154    // the list, if any.
155
156    if(!node.isArray())
157    {
158      return node;
159    }
160
161    if(node.size() == 0)
162    {
163      return null;
164    }
165
166    Iterator<JsonNode> i = node.elements();
167    while(i.hasNext())
168    {
169      JsonNode value = i.next();
170      JsonNode primary = value.get("primary");
171      if(primary != null && primary.booleanValue())
172      {
173        return value;
174      }
175    }
176    return node.get(0);
177  }
178
179}