AuditJsonConfig.java
/*
* 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 legal/CDDLv1.0.txt. See the License for the
* specific language governing permission and limitations under the License.
*
* When distributing Covered Software, include this CDDL Header Notice in each file and include
* the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
* Header, with the fields enclosed by brackets [] replaced by your own identifying
* information: "Portions copyright [year] [name of copyright owner]".
*
* Copyright 2015-2016 ForgeRock AS.
*/
package org.forgerock.audit.json;
import static org.forgerock.json.JsonValue.field;
import static org.forgerock.json.JsonValue.json;
import static org.forgerock.json.JsonValue.object;
import java.io.IOException;
import java.io.InputStream;
import java.util.LinkedHashMap;
import java.util.Map;
import org.forgerock.audit.AuditException;
import org.forgerock.audit.AuditServiceBuilder;
import org.forgerock.audit.AuditServiceConfiguration;
import org.forgerock.audit.events.handlers.AuditEventHandler;
import org.forgerock.audit.events.handlers.EventHandlerConfiguration;
import org.forgerock.audit.util.JsonValueUtils;
import org.forgerock.json.JsonValue;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
import com.fasterxml.jackson.databind.AnnotationIntrospector;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.introspect.Annotated;
import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector;
import com.fasterxml.jackson.module.jsonSchema.JsonSchema;
import com.fasterxml.jackson.module.jsonSchema.factories.SchemaFactoryWrapper;
/**
* Utility class to facilitate creation and configuration of audit service and audit event handlers
* through JSON.
*/
public final class AuditJsonConfig {
private static final Logger LOGGER = LoggerFactory.getLogger(AuditJsonConfig.class);
/** Field containing the name of an event handler. */
private static final String NAME_FIELD = "name";
/** Field containing the implementation class of an event handler. */
private static final String CLASS_FIELD = "class";
/** Field containing the configuration of an event handler. */
private static final String CONFIG_FIELD = "config";
/** Field containing events topics to process for an event handler. */
private static final String EVENTS_FIELD = "events";
/** The mapper from JSON structure to Java object. */
private static final ObjectMapper MAPPER = new ObjectMapper();
private static final AnnotationIntrospector DEFAULT_ANNOTATION_INTROSPECTOR = new JacksonAnnotationIntrospector();
private static final AnnotationIntrospector HELP_APPENDER_ANNOTATION_INTROSPECTOR =
new HelpAppenderAnnotationIntrospector();
private AuditJsonConfig() {
// prevent instantiation of the class
}
/**
* Returns a JSON value from the provided input stream.
*
* @param input
* Input stream containing an arbitrary JSON structure.
* @return the JSON value corresponding to the JSON structure
* @throws AuditException
* If an error occurs.
*/
public static JsonValue getJson(InputStream input) throws AuditException {
if (input == null) {
throw new AuditException("Input stream is null");
}
try {
Object val = MAPPER.readValue(input, LinkedHashMap.class);
return new JsonValue(val);
} catch (IOException e) {
throw new AuditException(String.format("Unable to retrieve json value from json input stream"), e);
}
}
/**
* Returns the audit service configuration from the provided input stream.
*
* @param input
* Input stream containing JSON configuration of the audit service.
* @return the configuration object
* @throws AuditException
* If any error occurs.
*/
public static AuditServiceConfiguration parseAuditServiceConfiguration(InputStream input) throws AuditException {
try {
return MAPPER.readValue(input, AuditServiceConfiguration.class);
} catch (IOException e) {
throw new AuditException(String.format("Unable to retrieve class %s from json input stream",
AuditServiceConfiguration.class), e);
}
}
/**
* Returns the audit service configuration from the provided JSON string.
*
* @param json
* JSON string representing the configuration of the audit service.
* @return the configuration object
* @throws AuditException
* If any error occurs.
*/
public static AuditServiceConfiguration parseAuditServiceConfiguration(String json) throws AuditException {
if (json == null) {
return new AuditServiceConfiguration();
}
try {
return MAPPER.readValue(json, AuditServiceConfiguration.class);
} catch (IOException e) {
throw new AuditException(String.format("Unable to retrieve class %s from json: %s",
AuditServiceConfiguration.class, json), e);
}
}
/**
* Returns the audit service configuration from the provided JSON value.
*
* @param json
* JSON value representing the configuration of the audit service.
* @return the configuration object
* @throws AuditException
* If any error occurs.
*/
public static AuditServiceConfiguration parseAuditServiceConfiguration(JsonValue json) throws AuditException {
return parseAuditServiceConfiguration(JsonValueUtils.extractValueAsString(json, "/"));
}
/**
* Configures and registers the audit event handler corresponding to the provided JSON configuration
* to the provided audit service.
*
* @param jsonConfig
* The configuration of the audit event handler as JSON.
* @param auditServiceBuilder
* The builder for the service the event handler will be registered to.
* @throws AuditException
* If any error occurs during configuration or registration of the handler.
*/
public static void registerHandlerToService(JsonValue jsonConfig, AuditServiceBuilder auditServiceBuilder)
throws AuditException {
registerHandlerToService(jsonConfig, auditServiceBuilder, auditServiceBuilder.getClass().getClassLoader());
}
/**
* Configures and registers the audit event handler corresponding to the provided JSON configuration
* to the provided audit service, using a specific class loader.
*
* @param jsonConfig
* The configuration of the audit event handler as JSON.
* @param auditServiceBuilder
* The builder for the service the event handler will be registered to.
* @param classLoader
* The class loader to use to load the handler and its configuration class.
* @throws AuditException
* If any error occurs during configuration or registration of the handler.
*/
public static void registerHandlerToService(JsonValue jsonConfig,
AuditServiceBuilder auditServiceBuilder, ClassLoader classLoader) throws AuditException {
String name = getHandlerName(jsonConfig);
Class<? extends AuditEventHandler> handlerClass = getAuditEventHandlerClass(name, jsonConfig, classLoader);
Class<? extends EventHandlerConfiguration> configClass =
getAuditEventHandlerConfigurationClass(name, handlerClass, classLoader);
EventHandlerConfiguration configuration = parseAuditEventHandlerConfiguration(configClass, jsonConfig);
auditServiceBuilder.withAuditEventHandler(handlerClass, configuration);
}
/**
* Returns the name of the event handler corresponding to provided JSON configuration.
* <p>
* The JSON configuration is expected to contains a "name" field identifying the
* event handler, e.g.
* <pre>
* "name" : "passthrough"
* </pre>
*
* @param jsonConfig
* The JSON configuration of the event handler.
* @return the name of the event handler
* @throws AuditException
* If an error occurs.
*/
private static String getHandlerName(JsonValue jsonConfig) throws AuditException {
String name = jsonConfig.get(CONFIG_FIELD).get(NAME_FIELD).asString();
if (name == null) {
throw new AuditException(String.format("No name is defined for the provided audit handler. "
+ "You must define a 'name' property in the configuration."));
}
return name;
}
/**
* Creates an audit event handler factory from the provided JSON configuration.
* <p>
* The JSON configuration is expected to contains a "class" property which provides
* the class name for the handler factory to instantiate.
*
* @param jsonConfig
* The configuration of the audit event handler as JSON.
* @param classLoader
* The class loader to use to load the handler and its configuration class.
* @return the fully configured audit event handler
* @throws AuditException
* If any error occurs during configuration or registration of the handler.
*/
@SuppressWarnings("unchecked") // Class.forName calls
private static Class<? extends AuditEventHandler> getAuditEventHandlerClass(
String handlerName, JsonValue jsonConfig, ClassLoader classLoader) throws AuditException {
// TODO: class name should not be provided in customer configuration
// but through a commons module/service context
String className = jsonConfig.get(CLASS_FIELD).asString();
if (className == null) {
String errorMessage = String.format("No class is defined for the audit handler %s. "
+ "You must define a 'class' property in the configuration.", handlerName);
throw new AuditException(errorMessage);
}
try {
return (Class<? extends AuditEventHandler>) Class.forName(className, true, classLoader);
} catch (ClassNotFoundException e) {
String errorMessage = String.format("Invalid class is defined for the audit handler %s.", handlerName);
throw new AuditException(errorMessage, e);
}
}
@SuppressWarnings("unchecked") // Class.forName calls
private static Class<? extends EventHandlerConfiguration> getAuditEventHandlerConfigurationClass(
String handlerName, Class<? extends AuditEventHandler> handlerClass, ClassLoader classLoader)
throws AuditException {
String className = handlerClass.getName() + "Configuration";
try {
return (Class<? extends EventHandlerConfiguration>) Class.forName(className, true, classLoader);
} catch (ClassNotFoundException e) {
String errorMessage = String.format("Unable to locate configuration class %s for the audit handler %s.",
className, handlerName);
throw new AuditException(errorMessage, e);
}
}
/**
* Returns the audit event handler configuration from the provided JSON string.
*
* @param <C>
* The type of the configuration bean for the event handler.
* @param jsonConfig
* The configuration of the audit event handler as JSON.
* @param clazz The class for type {@code <C>}.
* @return the fully configured audit event handler
* @throws AuditException
* If any error occurs while instantiating the configuration from JSON.
*/
public static <C extends EventHandlerConfiguration> C parseAuditEventHandlerConfiguration(
Class<C> clazz, JsonValue jsonConfig) throws AuditException {
C configuration = null;
JsonValue conf = jsonConfig.get(CONFIG_FIELD);
if (conf != null) {
configuration = MAPPER.convertValue(conf.getObject(), clazz);
}
return configuration;
}
/**
* Gets the configuration schema for an audit event handler as json schema. The supplied json config must contain
* a field called class with the value of the audit event handler implementation class.
* @param className The class name to get the configuration for.
* @param classLoader The {@link ClassLoader} to use to load the event handler and event handler config class.
* @return The config schema as json schema.
* @throws AuditException If any error occurs parsing the config class for schema.
*/
public static JsonValue getAuditEventHandlerConfigurationSchema(final String className,
final ClassLoader classLoader) throws AuditException {
final Class<? extends EventHandlerConfiguration> eventHandlerConfiguration =
getAuditEventHandlerConfigurationClass(
className,
getAuditEventHandlerClass(
className,
json(object(field("class", className))),
classLoader),
classLoader);
try {
MAPPER.setAnnotationIntrospector(HELP_APPENDER_ANNOTATION_INTROSPECTOR);
SchemaFactoryWrapper visitor = new SchemaFactoryWrapper();
MAPPER.acceptJsonFormatVisitor(MAPPER.constructType(eventHandlerConfiguration), visitor);
JsonSchema jsonSchema = visitor.finalSchema();
final JsonValue schema = json(MAPPER.readValue(MAPPER.writeValueAsString(jsonSchema), Map.class));
MAPPER.setAnnotationIntrospector(DEFAULT_ANNOTATION_INTROSPECTOR);
return schema;
} catch (IOException e) {
final String error = String.format("Unable to parse configuration class schema for configuration class %s",
eventHandlerConfiguration.getName());
LOGGER.error(error, e);
throw new AuditException(error, e);
}
}
/**
* Extends the default {@link JacksonAnnotationIntrospector} and overrides the {@link JsonPropertyDescription}
* annotation inorder to append ".help" to the description.
*/
private static class HelpAppenderAnnotationIntrospector extends JacksonAnnotationIntrospector {
@Override
public String findPropertyDescription(Annotated ann) {
JsonPropertyDescription desc = _findAnnotation(ann, JsonPropertyDescription.class);
return (desc == null) ? null : desc.value().concat(".help");
}
}
}