/* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright (c) 2011-2015 ForgeRock AS. All Rights Reserved * * The contents of this file are subject to the terms * of the Common Development and Distribution License * (the License). You may not use this file except in * compliance with the License. * * You can obtain a copy of the License at * http://forgerock.org/license/CDDLv1.0.html * See the License for the specific language governing * permission and limitations under the License. * * When distributing Covered Code, include this CDDL * Header Notice in each file and include the License file * at http://forgerock.org/license/CDDLv1.0.html * If applicable, add the following below the CDDL Header, * with the fields enclosed by brackets [] replaced by * your own identifying information: * "Portions Copyrighted [year] [name of copyright owner]" */ package org.forgerock.openidm.util; import java.io.IOException; import java.net.URL; import java.util.Arrays; import com.fasterxml.jackson.core.JsonGenerationException; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.util.DefaultPrettyPrinter; import com.fasterxml.jackson.core.util.DefaultPrettyPrinter.Indenter; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectWriter; import com.fasterxml.jackson.databind.SerializationFeature; import org.forgerock.json.JsonException; import org.forgerock.json.JsonPointer; import org.forgerock.json.JsonTransformer; import org.forgerock.json.JsonValue; import org.forgerock.json.JsonValueException; import org.forgerock.json.crypto.JsonCrypto; import org.forgerock.openidm.core.PropertyAccessor; import org.forgerock.openidm.core.PropertyUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public final class JsonUtil { private static final ObjectMapper OBJECT_MAPPER; private static final ObjectWriter PRETTY_WRITER; static { OBJECT_MAPPER = new ObjectMapper().configure( JsonParser.Feature.ALLOW_COMMENTS, true).disable( DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES).enable( SerializationFeature.INDENT_OUTPUT).enable( MapperFeature.SORT_PROPERTIES_ALPHABETICALLY); // TODO Make it configurable for Audit service // .configure(JsonGenerator.Feature.WRITE_NUMBERS_AS_STRINGS, true); Indenter indenter = new PrettyIndenter(); DefaultPrettyPrinter prettyPrinter = new DefaultPrettyPrinter(); prettyPrinter.indentObjectsWith(indenter); prettyPrinter.indentArraysWith(indenter); PRETTY_WRITER = OBJECT_MAPPER.writer(prettyPrinter); } /** * Setup logging for the {@link JsonUtil}. Only for diagnostic reason! */ private final static Logger logger = LoggerFactory.getLogger(JsonUtil.class); private JsonUtil() { } public static boolean jsonIsNull(JsonValue value) { return (value == null || value.isNull()); } public static boolean isEncrypted(String value) { boolean encrypted = false; // TODO: delegate the sanity check if String is a candidate for parsing // to the crypto lib boolean candidate = value != null && value.startsWith("{\"$crypto\":{") && value.endsWith("}}"); if (candidate) { try { JsonValue jsonValue = parseStringified(value); encrypted = JsonCrypto.isJsonCrypto(jsonValue); } catch (JsonException ex) { encrypted = false; // IF we can't parse the string assume it's // not in an encrypted format we support } } return encrypted; } public static ObjectMapper build() { final ObjectMapper mapper = OBJECT_MAPPER.copy(); mapper.getFactory().configure(JsonParser.Feature.ALLOW_COMMENTS, true); return mapper; } public static String writeValueAsString(JsonValue value) throws JsonProcessingException { return OBJECT_MAPPER.writeValueAsString(value.getObject()); } public static String writePrettyValueAsString(JsonValue value) throws JsonProcessingException { return PRETTY_WRITER.writeValueAsString(value.getObject()); } /** * * @param content * @return */ public static JsonValue parseStringified(String content) { JsonValue jsonValue = null; try { Object parsedValue = OBJECT_MAPPER.readValue(content, Object.class); jsonValue = new JsonValue(parsedValue); } catch (IOException ex) { throw new JsonException("String passed into parsing is not valid JSON", ex); } return jsonValue; } /** * * @param content * @return */ public static JsonValue parseURL(URL content) { JsonValue jsonValue = null; try { Object parsedValue = OBJECT_MAPPER.readValue(content, Object.class); jsonValue = new JsonValue(parsedValue); } catch (IOException ex) { throw new JsonException("URL passed into parsing is not valid JSON", ex); } return jsonValue; } public static JsonTransformer getPropertyJsonTransformer(final JsonValue properties, boolean allowUnresolved) { if (jsonIsNull(properties)) { return null; } return new PropertyTransformer(properties, allowUnresolved); } public static class PropertyTransformer implements JsonTransformer { private final PropertyAccessor properties; private final boolean eager; PropertyTransformer(final JsonValue properties, boolean allowUnresolved) { this.eager = !allowUnresolved; this.properties = new PropertyAccessor() { @Override @SuppressWarnings("unchecked") public <T> T getProperty(String key, T defaultValue, Class<T> expected) { JsonPointer pointer = new JsonPointer(key.split("\\.")); try { JsonValue newValue = properties.get(pointer); if (null != newValue) { return (T) newValue.required().expect(expected).getObject(); } } catch (JsonValueException e) { logger.trace("Failed to substitute variable {}", key, e); /* * Expected if the value is null or the type does not * match */ } catch (Throwable t) { logger.debug("Failed to substitute variable with unexpected error {}", key, t); } if (eager && null == defaultValue) { StringBuilder sb = new StringBuilder("Failed to resolve mandatory property: ") .append(key); if (null != expected && !Object.class.equals(expected)) { sb.append(" expecting ").append(expected.getSimpleName()).append( " class"); } throw new JsonValueException(null, sb.toString()); } return defaultValue; } }; } /** * {@inheritDoc} */ @Override public void transform(JsonValue value) { if (null != value && value.isString()) { try { value.setObject(PropertyUtil.substVars(value.asString(), properties, PropertyUtil.Delimiter.DOLLAR, false)); } catch (JsonValueException e) { throw new JsonValueException(value, e.getMessage()); } } } } /** * Indenter, part of formatting Jackson output in pretty print Makes the * number of spaces to use per indent configurable. * */ private static class PrettyIndenter implements Indenter { // Default to 4 spaces per level int noOfSpaces = 4; final static String SYSTEM_LINE_SEPARATOR; static { String lf = null; try { lf = System.getProperty("line.separator"); } catch (Throwable t) { } // access exception? SYSTEM_LINE_SEPARATOR = (lf == null) ? "\n" : lf; } final static int SPACE_COUNT = 64; final static char[] SPACES = new char[SPACE_COUNT]; static { Arrays.fill(SPACES, ' '); } /** * Configure how many spaces to use per indent. Default is 4 spaces. * * @param noOfSpaces */ public void setIndentSpaces(int noOfSpaces) { this.noOfSpaces = noOfSpaces; } public boolean isInline() { return false; } @Override public void writeIndentation(JsonGenerator jg, int level) throws IOException, JsonGenerationException { jg.writeRaw(SYSTEM_LINE_SEPARATOR); level = level * noOfSpaces; while (level > SPACE_COUNT) { // should never happen but... jg.writeRaw(SPACES, 0, SPACE_COUNT); level -= SPACES.length; } jg.writeRaw(SPACES, 0, level); } } }