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 */
016package org.forgerock.audit.json;
017
018import static org.forgerock.json.JsonValue.field;
019import static org.forgerock.json.JsonValue.json;
020import static org.forgerock.json.JsonValue.object;
021
022import java.io.IOException;
023import java.io.InputStream;
024import java.util.LinkedHashMap;
025import java.util.Map;
026
027import org.forgerock.audit.AuditException;
028import org.forgerock.audit.AuditServiceBuilder;
029import org.forgerock.audit.AuditServiceConfiguration;
030import org.forgerock.audit.events.handlers.AuditEventHandler;
031import org.forgerock.audit.events.handlers.EventHandlerConfiguration;
032import org.forgerock.audit.util.JsonValueUtils;
033import org.forgerock.json.JsonValue;
034import org.slf4j.Logger;
035import org.slf4j.LoggerFactory;
036
037import com.fasterxml.jackson.annotation.JsonPropertyDescription;
038import com.fasterxml.jackson.databind.AnnotationIntrospector;
039import com.fasterxml.jackson.databind.ObjectMapper;
040import com.fasterxml.jackson.databind.introspect.Annotated;
041import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector;
042import com.fasterxml.jackson.module.jsonSchema.JsonSchema;
043import com.fasterxml.jackson.module.jsonSchema.factories.SchemaFactoryWrapper;
044
045/**
046 * Utility class to facilitate creation and configuration of audit service and audit event handlers
047 * through JSON.
048 */
049public final class AuditJsonConfig {
050    private static final Logger LOGGER = LoggerFactory.getLogger(AuditJsonConfig.class);
051
052    /** Field containing the name of an event handler. */
053    private static final String NAME_FIELD = "name";
054    /** Field containing the implementation class of an event handler. */
055    private static final String CLASS_FIELD = "class";
056    /** Field containing the configuration of an event handler. */
057    private static final String CONFIG_FIELD = "config";
058    /** Field containing events topics to process for an event handler. */
059    private static final String EVENTS_FIELD = "events";
060
061    /** The mapper from JSON structure to Java object. */
062    private static final ObjectMapper MAPPER = new ObjectMapper();
063
064    private static final AnnotationIntrospector DEFAULT_ANNOTATION_INTROSPECTOR = new JacksonAnnotationIntrospector();
065    private static final AnnotationIntrospector HELP_APPENDER_ANNOTATION_INTROSPECTOR =
066            new HelpAppenderAnnotationIntrospector();
067
068    private AuditJsonConfig() {
069        // prevent instantiation of the class
070    }
071
072    /**
073     * Returns a JSON value from the provided input stream.
074     *
075     * @param input
076     *          Input stream containing an arbitrary JSON structure.
077     * @return the JSON value corresponding to the JSON structure
078     * @throws AuditException
079     *          If an error occurs.
080     */
081    public static JsonValue getJson(InputStream input) throws AuditException {
082        if (input == null) {
083            throw new AuditException("Input stream is null");
084        }
085        try {
086            Object val = MAPPER.readValue(input, LinkedHashMap.class);
087            return new JsonValue(val);
088        } catch (IOException e) {
089            throw new AuditException(String.format("Unable to retrieve json value from json input stream"), e);
090        }
091    }
092
093    /**
094     * Returns the audit service configuration from the provided input stream.
095     *
096     * @param input
097     *          Input stream containing JSON configuration of the audit service.
098     * @return the configuration object
099     * @throws AuditException
100     *          If any error occurs.
101     */
102    public static AuditServiceConfiguration parseAuditServiceConfiguration(InputStream input) throws AuditException {
103        try {
104            return MAPPER.readValue(input, AuditServiceConfiguration.class);
105        } catch (IOException e) {
106            throw new AuditException(String.format("Unable to retrieve class %s from json input stream",
107                    AuditServiceConfiguration.class), e);
108        }
109    }
110
111    /**
112     * Returns the audit service configuration from the provided JSON string.
113     *
114     * @param json
115     *          JSON string representing the configuration of the audit service.
116     * @return the configuration object
117     * @throws AuditException
118     *          If any error occurs.
119     */
120    public static AuditServiceConfiguration parseAuditServiceConfiguration(String json) throws AuditException {
121        if (json == null) {
122            return new AuditServiceConfiguration();
123        }
124        try {
125            return MAPPER.readValue(json, AuditServiceConfiguration.class);
126        } catch (IOException e) {
127            throw new AuditException(String.format("Unable to retrieve class %s from json: %s",
128                    AuditServiceConfiguration.class, json), e);
129        }
130    }
131
132    /**
133     * Returns the audit service configuration from the provided JSON value.
134     *
135     * @param json
136     *          JSON value representing the configuration of the audit service.
137     * @return the configuration object
138     * @throws AuditException
139     *          If any error occurs.
140     */
141    public static AuditServiceConfiguration parseAuditServiceConfiguration(JsonValue json) throws AuditException {
142        return parseAuditServiceConfiguration(JsonValueUtils.extractValueAsString(json, "/"));
143    }
144
145    /**
146     * Configures and registers the audit event handler corresponding to the provided JSON configuration
147     * to the provided audit service.
148     *
149     * @param jsonConfig
150     *          The configuration of the audit event handler as JSON.
151     * @param auditServiceBuilder
152     *          The builder for the service the event handler will be registered to.
153     * @throws AuditException
154     *             If any error occurs during configuration or registration of the handler.
155     */
156    public static void registerHandlerToService(JsonValue jsonConfig, AuditServiceBuilder auditServiceBuilder)
157            throws AuditException {
158        registerHandlerToService(jsonConfig, auditServiceBuilder, auditServiceBuilder.getClass().getClassLoader());
159    }
160
161    /**
162     * Configures and registers the audit event handler corresponding to the provided JSON configuration
163     * to the provided audit service, using a specific class loader.
164     *
165     * @param jsonConfig
166     *          The configuration of the audit event handler as JSON.
167     * @param auditServiceBuilder
168     *          The builder for the service the event handler will be registered to.
169     * @param classLoader
170     *          The class loader to use to load the handler and its configuration class.
171     * @throws AuditException
172     *             If any error occurs during configuration or registration of the handler.
173     */
174    public static void registerHandlerToService(JsonValue jsonConfig,
175            AuditServiceBuilder auditServiceBuilder, ClassLoader classLoader) throws AuditException {
176        String name = getHandlerName(jsonConfig);
177        Class<? extends AuditEventHandler> handlerClass = getAuditEventHandlerClass(name, jsonConfig, classLoader);
178        Class<? extends EventHandlerConfiguration> configClass =
179                getAuditEventHandlerConfigurationClass(name, handlerClass, classLoader);
180        EventHandlerConfiguration configuration = parseAuditEventHandlerConfiguration(configClass, jsonConfig);
181        auditServiceBuilder.withAuditEventHandler(handlerClass, configuration);
182    }
183
184    /**
185     * Returns the name of the event handler corresponding to provided JSON configuration.
186     * <p>
187     * The JSON configuration is expected to contains a "name" field identifying the
188     * event handler, e.g.
189     * <pre>
190     *  "name" : "passthrough"
191     * </pre>
192     *
193     * @param jsonConfig
194     *          The JSON configuration of the event handler.
195     * @return the name of the event handler
196     * @throws AuditException
197     *          If an error occurs.
198     */
199    private static String getHandlerName(JsonValue jsonConfig) throws AuditException {
200        String name = jsonConfig.get(CONFIG_FIELD).get(NAME_FIELD).asString();
201        if (name == null) {
202            throw new AuditException(String.format("No name is defined for the provided audit handler. "
203                    + "You must define a 'name' property in the configuration."));
204        }
205        return name;
206    }
207
208    /**
209     * Creates an audit event handler factory from the provided JSON configuration.
210     * <p>
211     * The JSON configuration is expected to contains a "class" property which provides
212     * the class name for the handler factory to instantiate.
213     *
214     * @param jsonConfig
215     *          The configuration of the audit event handler as JSON.
216     * @param classLoader
217     *          The class loader to use to load the handler and its configuration class.
218     * @return the fully configured audit event handler
219     * @throws AuditException
220     *             If any error occurs during configuration or registration of the handler.
221     */
222    @SuppressWarnings("unchecked") // Class.forName calls
223    private static Class<? extends AuditEventHandler> getAuditEventHandlerClass(
224            String handlerName, JsonValue jsonConfig, ClassLoader classLoader) throws AuditException {
225        // TODO: class name should not be provided in customer configuration
226        // but through a commons module/service context
227        String className = jsonConfig.get(CLASS_FIELD).asString();
228        if (className == null) {
229            String errorMessage = String.format("No class is defined for the audit handler %s. "
230                    + "You must define a 'class' property in the configuration.", handlerName);
231            throw new AuditException(errorMessage);
232        }
233        try {
234            return (Class<? extends AuditEventHandler>) Class.forName(className, true, classLoader);
235        } catch (ClassNotFoundException e) {
236            String errorMessage = String.format("Invalid class is defined for the audit handler %s.", handlerName);
237            throw new AuditException(errorMessage, e);
238        }
239    }
240
241    @SuppressWarnings("unchecked") // Class.forName calls
242    private static Class<? extends EventHandlerConfiguration> getAuditEventHandlerConfigurationClass(
243            String handlerName, Class<? extends AuditEventHandler> handlerClass, ClassLoader classLoader)
244            throws AuditException {
245        String className = handlerClass.getName() + "Configuration";
246        try {
247            return (Class<? extends EventHandlerConfiguration>) Class.forName(className, true, classLoader);
248        } catch (ClassNotFoundException e) {
249            String errorMessage = String.format("Unable to locate configuration class %s for the audit handler %s.",
250                    className, handlerName);
251            throw new AuditException(errorMessage, e);
252        }
253    }
254
255    /**
256     * Returns the audit event handler configuration from the provided JSON string.
257     *
258     * @param <C>
259     *          The type of the configuration bean for the event handler.
260     * @param jsonConfig
261     *          The configuration of the audit event handler as JSON.
262     * @param clazz The class for type {@code <C>}.
263     * @return the fully configured audit event handler
264     * @throws AuditException
265     *             If any error occurs while instantiating the configuration from JSON.
266     */
267    public static <C extends EventHandlerConfiguration> C parseAuditEventHandlerConfiguration(
268            Class<C> clazz, JsonValue jsonConfig) throws AuditException {
269        C configuration = null;
270        JsonValue conf = jsonConfig.get(CONFIG_FIELD);
271        if (conf != null) {
272            configuration = MAPPER.convertValue(conf.getObject(), clazz);
273        }
274        return configuration;
275    }
276
277    /**
278     * Gets the configuration schema for an audit event handler as json schema. The supplied json config must contain
279     * a field called class with the value of the audit event handler implementation class.
280     * @param className The class name to get the configuration for.
281     * @param classLoader The {@link ClassLoader} to use to load the event handler and event handler config class.
282     * @return The config schema as json schema.
283     * @throws AuditException If any error occurs parsing the config class for schema.
284     */
285    public static JsonValue getAuditEventHandlerConfigurationSchema(final String className,
286            final ClassLoader classLoader) throws AuditException {
287        final Class<? extends EventHandlerConfiguration> eventHandlerConfiguration =
288                getAuditEventHandlerConfigurationClass(
289                        className,
290                        getAuditEventHandlerClass(
291                                className,
292                                json(object(field("class", className))),
293                                classLoader),
294                        classLoader);
295        try {
296            MAPPER.setAnnotationIntrospector(HELP_APPENDER_ANNOTATION_INTROSPECTOR);
297            SchemaFactoryWrapper visitor = new SchemaFactoryWrapper();
298            MAPPER.acceptJsonFormatVisitor(MAPPER.constructType(eventHandlerConfiguration), visitor);
299            JsonSchema jsonSchema = visitor.finalSchema();
300            final JsonValue schema = json(MAPPER.readValue(MAPPER.writeValueAsString(jsonSchema), Map.class));
301            MAPPER.setAnnotationIntrospector(DEFAULT_ANNOTATION_INTROSPECTOR);
302            return schema;
303        } catch (IOException e) {
304            final String error = String.format("Unable to parse configuration class schema for configuration class %s",
305                    eventHandlerConfiguration.getName());
306            LOGGER.error(error, e);
307            throw new AuditException(error, e);
308        }
309    }
310
311    /**
312     * Extends the default {@link JacksonAnnotationIntrospector} and overrides the {@link JsonPropertyDescription}
313     * annotation inorder to append ".help" to the description.
314     */
315    private static class HelpAppenderAnnotationIntrospector extends JacksonAnnotationIntrospector {
316        @Override
317        public String findPropertyDescription(Annotated ann) {
318            JsonPropertyDescription desc = _findAnnotation(ann, JsonPropertyDescription.class);
319            return (desc == null) ? null : desc.value().concat(".help");
320        }
321    }
322
323}