001/* 002 * The contents of this file are subject to the terms of the Common Development and 003 * Distribution License (the License). You may not use this file except in compliance with the 004 * License. 005 * 006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the 007 * specific language governing permission and limitations under the License. 008 * 009 * When distributing Covered Software, include this CDDL Header Notice in each file and include 010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL 011 * Header, with the fields enclosed by brackets [] replaced by your own identifying 012 * information: "Portions copyright [year] [name of copyright owner]". 013 * 014 * Copyright 2015-2016 ForgeRock AS. 015 */ 016 017package org.forgerock.audit.util; 018 019import java.util.ArrayList; 020import java.util.Arrays; 021import java.util.Collections; 022import java.util.LinkedHashMap; 023import java.util.List; 024import java.util.Locale; 025import java.util.Map; 026import java.util.Set; 027import java.util.TreeMap; 028 029import org.forgerock.json.JsonPointer; 030import org.forgerock.json.JsonValue; 031import org.forgerock.util.query.QueryFilter; 032import org.forgerock.util.query.QueryFilterVisitor; 033import org.slf4j.Logger; 034import org.slf4j.LoggerFactory; 035 036import com.fasterxml.jackson.core.JsonProcessingException; 037import com.fasterxml.jackson.databind.ObjectMapper; 038 039/** 040 * Contains some JsonValue Utility methods. 041 */ 042public final class JsonValueUtils { 043 044 private static final Logger LOGGER = LoggerFactory.getLogger(JsonValueUtils.class); 045 private static final ObjectMapper MAPPER = new ObjectMapper(); 046 047 private JsonValueUtils() { 048 // utility class 049 } 050 051 /** 052 * Expands a Json Object Map, where the keys of the map are the {@link JsonPointer JsonPointer }s 053 * and the values are the value the {@link JsonPointer JsonPointer } resolves to. 054 * 055 * For Example, the following key-value pairs 056 * <pre> 057 * /object/array/0 , "test" 058 * /object/array/1 , "test1" 059 * /string , "stringVal" 060 * /boolean , false 061 * /number , 1 062 * /array/0 , "value1" 063 * /array/1 , "value2" 064 * </pre> 065 * 066 * will produce the following json object 067 * <pre> 068 * { 069 * "object" : { 070 * "array" : ["test", "test1"] 071 * }, 072 * "string" : "stringVal", 073 * "boolean" : false, 074 * "number" : 1, 075 * "array" : ["value1", "value2"] 076 * } 077 * </pre> 078 * @param object the Json Object Map containing the {@link JsonPointer JsonPointer }s and values 079 * @return the {@link JsonValue JsonValue } expanded from the object map 080 */ 081 public static JsonValue expand(final Map<String, Object> object) { 082 //sort the objects so the array objects are in order 083 final Map<String, Object> sortedObjects = new TreeMap<>(object); 084 return buildObject(sortedObjects); 085 } 086 087 /** 088 * 089 * Flattens a {@link JsonValue JsonValue } to a Map, where the keys of the Map are {@link JsonPointer JsonPointer }s 090 * and the values are the value the {@link JsonPointer JsonPointer }s resolve to. 091 * 092 * For Example, the following JsonValue 093 * 094 * <pre> 095 * { 096 * "object" : { 097 * "array" : ["test", "test1"] 098 * }, 099 * "string" : "stringVal", 100 * "boolean" : false, 101 * "number" : 1, 102 * "array" : ["value1", "value2"] 103 * } 104 * </pre> 105 * 106 * will produce the following Map key-value pairs 107 * 108 * <pre> 109 * /object/array/0 , "test" 110 * /object/array/1 , "test1" 111 * /string , "stringVal" 112 * /boolean , false 113 * /number , 1 114 * /array/0 , "value1" 115 * /array/1 , "value2" 116 * </pre> 117 * @param jsonValue the {@link JsonValue JsonValue } object to flatten 118 * @return a Map representing the flattened {@link JsonValue JsonValue } object 119 */ 120 public static Map<String, Object> flatten(final JsonValue jsonValue) { 121 Map<String, Object> flatObject = new LinkedHashMap<>(); 122 flatten(new JsonPointer(), jsonValue, flatObject); 123 return flatObject; 124 } 125 126 /** 127 * Extracts String representation of field identified by <code>fieldName</code> from <code>json</code> object. 128 * 129 * @param json the {@link JsonValue} object from which to extract a value. 130 * @param fieldName the field identifier in a form consumable by {@link JsonPointer}. 131 * 132 * @return A non-null String representation of the field's value. If the specified field is not present or has 133 * a null value, an empty string will be returned. 134 */ 135 public static String extractValueAsString(final JsonValue json, final String fieldName) { 136 JsonValue value = json.get(new JsonPointer(fieldName)); 137 if (value == null || value.isNull()) { 138 return null; 139 } else if (value.isString()) { 140 return value.asString(); 141 } else { 142 String rawStr = null; 143 try { 144 rawStr = MAPPER.writeValueAsString(value.getObject()); 145 } catch (JsonProcessingException e) { 146 LOGGER.error("Unable to write the value for field {} as a string.", fieldName); 147 } 148 return rawStr; 149 } 150 } 151 152 private static JsonValue buildObject(Map<String, Object> objectSet) { 153 final JsonValue jsonValue = new JsonValue(new LinkedHashMap<>()); 154 for (Map.Entry<String, Object> entry : objectSet.entrySet()) { 155 final String key = entry.getKey(); 156 final Object value = entry.getValue(); 157 if (jsonValue.get(new JsonPointer(key)) != null 158 && !jsonValue.get(new JsonPointer(key)).isNull()) { 159 //only build a sub json object for one prefix value 160 continue; 161 } 162 final JsonPointer jsonPointer = new JsonPointer(key); 163 int numberOfIndexTokens = getIndexTokens(jsonPointer); 164 if (numberOfIndexTokens > 1) { 165 //more than one json array must build the sub json object 166 int firstIndexTokenPos = getNextIndexToken(jsonPointer, 0); 167 final JsonPointer prefix = subJsonPointer(jsonPointer, 0, firstIndexTokenPos + 1); 168 jsonValue.putPermissive( 169 replaceLastIndexToken(prefix), 170 buildObject(findObjectsThatMatchPrefix(prefix, objectSet)).getObject() 171 ); 172 } else { 173 jsonValue.putPermissive(replaceLastIndexToken(jsonPointer), value); 174 } 175 } 176 return jsonValue; 177 } 178 179 private static Map<String, Object> findObjectsThatMatchPrefix( 180 final JsonPointer prefix, 181 Map<String, Object> objectSet) { 182 Map<String, Object> matchingObjects = new LinkedHashMap<>(); 183 for (final String key : objectSet.keySet()) { 184 if (key.startsWith(prefix.toString())) { 185 matchingObjects.put(key.substring(prefix.toString().length(), key.length()), objectSet.get(key)); 186 } 187 } 188 return matchingObjects; 189 } 190 191 private static boolean isIndexToken(final String token) { 192 if (token.isEmpty()) { 193 return false; 194 } else { 195 for (int i = 0; i < token.length(); i++) { 196 final char c = token.charAt(i); 197 if (!Character.isDigit(c)) { 198 return false; 199 } 200 } 201 return true; 202 } 203 } 204 205 private static JsonPointer replaceLastIndexToken(final JsonPointer jsonPointer) { 206 final String[] jsonPointerTokens = jsonPointer.toArray(); 207 if (getNextIndexToken(jsonPointer, jsonPointer.size() - 1) != -1) { 208 jsonPointerTokens[jsonPointerTokens.length - 1] = "-"; 209 } 210 return new JsonPointer(jsonPointerTokens); 211 } 212 213 private static int getNextIndexToken(final JsonPointer jsonPointer, int start) { 214 final String[] jsonPointerTokens = jsonPointer.toArray(); 215 for (int i = start; i < jsonPointerTokens.length; i++) { 216 if (isIndexToken(jsonPointerTokens[i])) { 217 return i; 218 } 219 } 220 return -1; 221 } 222 223 private static JsonPointer subJsonPointer(final JsonPointer jsonPointer, final int start, final int end) { 224 final String[] jsonPointerTokens = jsonPointer.toArray(); 225 final List<String> newJsonPointerTokens = new ArrayList<>(); 226 for (int i = start; i < end; i++) { 227 newJsonPointerTokens.add(jsonPointerTokens[i]); 228 } 229 return new JsonPointer(newJsonPointerTokens.toArray(new String[newJsonPointerTokens.size()])); 230 } 231 232 private static int getIndexTokens(final JsonPointer jsonPointer) { 233 int numberOfIndexTokens = 0; 234 final String[] jsonPointerTokens = jsonPointer.toArray(); 235 for (int i = 0; i < jsonPointerTokens.length; i++) { 236 if (isIndexToken(jsonPointerTokens[i])) { 237 numberOfIndexTokens++; 238 } 239 } 240 return numberOfIndexTokens; 241 } 242 243 private static void flatten( 244 final JsonPointer pointer, 245 final JsonValue jsonValue, 246 final Map<String, Object> flatObject) { 247 final Set<String> jsonValueKeys = jsonValue.get(pointer).keys(); 248 for (final String key : jsonValueKeys) { 249 final JsonPointer keyPointer = concatJsonPointer(pointer, key); 250 final JsonValue temp = jsonValue.get(keyPointer); 251 if (temp.isMap() || temp.isList()) { 252 flatten(keyPointer, jsonValue, flatObject); 253 } else { 254 flatObject.put( 255 keyPointer.toString(), 256 jsonValue.get(keyPointer).getObject()); 257 } 258 } 259 return; 260 } 261 262 private static JsonPointer concatJsonPointer(final JsonPointer pointer, final String key) { 263 final String[] pointerTokens = pointer.toArray(); 264 final String[] newPointerTokens = Arrays.copyOf(pointerTokens, pointerTokens.length + 1); 265 newPointerTokens[pointerTokens.length] = key; 266 return new JsonPointer(newPointerTokens); 267 268 } 269 270 /** 271 * A generic JsonValue Query Filter Visitor. 272 */ 273 public static final QueryFilterVisitor<Boolean, JsonValue, JsonPointer> JSONVALUE_FILTER_VISITOR = 274 new QueryFilterVisitor<Boolean, JsonValue, JsonPointer>() { 275 @Override 276 public Boolean visitAndFilter(final JsonValue p, final List<QueryFilter<JsonPointer>> subFilters) { 277 for (final QueryFilter<JsonPointer> subFilter : subFilters) { 278 if (!subFilter.accept(this, p)) { 279 return Boolean.FALSE; 280 } 281 } 282 return Boolean.TRUE; 283 } 284 285 @Override 286 public Boolean visitBooleanLiteralFilter(final JsonValue p, final boolean value) { 287 return value; 288 } 289 290 @Override 291 public Boolean visitContainsFilter(final JsonValue p, final JsonPointer field, 292 final Object valueAssertion) { 293 for (final Object value : getValues(p, field)) { 294 if (isCompatible(valueAssertion, value)) { 295 if (valueAssertion instanceof String) { 296 final String s1 = ((String) valueAssertion).toLowerCase(Locale.ENGLISH); 297 final String s2 = ((String) value).toLowerCase(Locale.ENGLISH); 298 if (s2.contains(s1)) { 299 return Boolean.TRUE; 300 } 301 } else { 302 // Use equality matching for numbers and booleans. 303 if (compareValues(valueAssertion, value) == 0) { 304 return Boolean.TRUE; 305 } 306 } 307 } 308 } 309 return Boolean.FALSE; 310 } 311 312 @Override 313 public Boolean visitEqualsFilter(final JsonValue p, final JsonPointer field, 314 final Object valueAssertion) { 315 Boolean result = Boolean.TRUE; 316 for (final Object value : getValues(p, field)) { 317 if (!isCompatible(valueAssertion, value) || compareValues(valueAssertion, value) != 0) { 318 result = Boolean.FALSE; 319 } 320 } 321 return result; 322 } 323 324 @Override 325 public Boolean visitExtendedMatchFilter(final JsonValue p, final JsonPointer field, 326 final String matchingRuleId, final Object valueAssertion) { 327 // Extended filters are not supported 328 return Boolean.FALSE; 329 } 330 331 @Override 332 public Boolean visitGreaterThanFilter(final JsonValue p, final JsonPointer field, 333 final Object valueAssertion) { 334 for (final Object value : getValues(p, field)) { 335 if (isCompatible(valueAssertion, value) && compareValues(valueAssertion, value) < 0) { 336 return Boolean.TRUE; 337 } 338 } 339 return Boolean.FALSE; 340 } 341 342 @Override 343 public Boolean visitGreaterThanOrEqualToFilter(final JsonValue p, final JsonPointer field, 344 final Object valueAssertion) { 345 for (final Object value : getValues(p, field)) { 346 if (isCompatible(valueAssertion, value) && compareValues(valueAssertion, value) <= 0) { 347 return Boolean.TRUE; 348 } 349 } 350 return Boolean.FALSE; 351 } 352 353 @Override 354 public Boolean visitLessThanFilter(final JsonValue p, final JsonPointer field, 355 final Object valueAssertion) { 356 for (final Object value : getValues(p, field)) { 357 if (isCompatible(valueAssertion, value) && compareValues(valueAssertion, value) > 0) { 358 return Boolean.TRUE; 359 } 360 } 361 return Boolean.FALSE; 362 } 363 364 @Override 365 public Boolean visitLessThanOrEqualToFilter(final JsonValue p, final JsonPointer field, 366 final Object valueAssertion) { 367 for (final Object value : getValues(p, field)) { 368 if (isCompatible(valueAssertion, value) && compareValues(valueAssertion, value) >= 0) { 369 return Boolean.TRUE; 370 } 371 } 372 return Boolean.FALSE; 373 } 374 375 @Override 376 public Boolean visitNotFilter(final JsonValue p, final QueryFilter<JsonPointer> subFilter) { 377 return !subFilter.accept(this, p); 378 } 379 380 @Override 381 public Boolean visitOrFilter(final JsonValue p, final List<QueryFilter<JsonPointer>> subFilters) { 382 for (final QueryFilter<JsonPointer> subFilter : subFilters) { 383 if (subFilter.accept(this, p)) { 384 return Boolean.TRUE; 385 } 386 } 387 return Boolean.FALSE; 388 } 389 390 @Override 391 public Boolean visitPresentFilter(final JsonValue p, final JsonPointer field) { 392 final JsonValue value = p.get(field); 393 return value != null; 394 } 395 396 @Override 397 public Boolean visitStartsWithFilter(final JsonValue p, final JsonPointer field, 398 final Object valueAssertion) { 399 for (final Object value : getValues(p, field)) { 400 if (isCompatible(valueAssertion, value)) { 401 if (valueAssertion instanceof String) { 402 final String s1 = ((String) valueAssertion).toLowerCase(Locale.ENGLISH); 403 final String s2 = ((String) value).toLowerCase(Locale.ENGLISH); 404 if (s2.startsWith(s1)) { 405 return Boolean.TRUE; 406 } 407 } else { 408 // Use equality matching for numbers and booleans. 409 if (compareValues(valueAssertion, value) == 0) { 410 return Boolean.TRUE; 411 } 412 } 413 } 414 } 415 return Boolean.FALSE; 416 } 417 418 private List<Object> getValues(final JsonValue resource, final JsonPointer field) { 419 final JsonValue value = resource.get(field); 420 if (value == null) { 421 return Collections.emptyList(); 422 } else if (value.isList()) { 423 return value.asList(); 424 } else { 425 return Collections.singletonList(value.getObject()); 426 } 427 } 428 429 private int compareValues(final Object v1, final Object v2) { 430 if (v1 instanceof String && v2 instanceof String) { 431 final String s1 = (String) v1; 432 final String s2 = (String) v2; 433 return s1.compareToIgnoreCase(s2); 434 } else if (v1 instanceof Number && v2 instanceof Number) { 435 final Double n1 = ((Number) v1).doubleValue(); 436 final Double n2 = ((Number) v2).doubleValue(); 437 return n1.compareTo(n2); 438 } else if (v1 instanceof Boolean && v2 instanceof Boolean) { 439 final Boolean b1 = (Boolean) v1; 440 final Boolean b2 = (Boolean) v2; 441 return b1.compareTo(b2); 442 } else { 443 // Different types: we need to ensure predictable ordering, 444 // so use class name as secondary key. 445 return v1.getClass().getName().compareTo(v2.getClass().getName()); 446 } 447 } 448 449 private boolean isCompatible(final Object v1, final Object v2) { 450 return (v1 instanceof String && v2 instanceof String) 451 || (v1 instanceof Number && v2 instanceof Number) 452 || (v1 instanceof Boolean && v2 instanceof Boolean); 453 } 454 455 }; 456}