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}