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.events;
017
018import static org.forgerock.json.JsonValue.json;
019import static org.forgerock.json.JsonValue.object;
020
021import com.fasterxml.jackson.databind.ObjectMapper;
022import org.forgerock.json.JsonValue;
023import org.slf4j.Logger;
024import org.slf4j.LoggerFactory;
025
026import java.io.BufferedInputStream;
027import java.io.IOException;
028import java.io.InputStream;
029import java.util.HashMap;
030import java.util.Map;
031
032/**
033 * Builder for {@link EventTopicsMetaData}.
034 */
035public final class EventTopicsMetaDataBuilder {
036
037    private static final Logger logger = LoggerFactory.getLogger(EventTopicsMetaDataBuilder.class);
038    private static final String SCHEMA = "schema";
039    private static final String PROPERTIES = "properties";
040    private static final ObjectMapper MAPPER = new ObjectMapper();
041
042    private JsonValue coreTopicSchemaExtensions = json(object());
043    private JsonValue additionalTopicSchemas = json(object());
044
045    private EventTopicsMetaDataBuilder() {
046        // private to force use of static factory method
047    }
048
049    /**
050     * Create a new instance of EventTopicsMetaDataBuilder that will populate {@link EventTopicsMetaData} objects its
051     * creates with the schema meta-data for core topics.
052     *
053     * @return a new instance of EventTopicsMetaDataBuilder.
054     */
055    public static EventTopicsMetaDataBuilder coreTopicSchemas() {
056        return new EventTopicsMetaDataBuilder();
057    }
058
059    /**
060     * Specifies additional fields that should be added to the schemas for core event topics.
061     * <p/>
062     * The extension must not redefine a property already defined in the core event topics.
063     * <p>
064     * Example of a valid extension:
065     * <pre>
066     *  {
067     *    "access": {
068     *      "schema": {
069     *        "$schema": "http://json-schema.org/draft-04/schema#",
070     *        "id": "/",
071     *        "type": "object",
072     *        "properties": {
073     *          "extraField": {
074     *            "type": "string"
075     *          }
076     *        }
077     *      }
078     *    }
079     *  }
080     * </pre>
081     *
082     * @param coreTopicSchemaExtensions
083     *          the extension of the core event topics.
084     * @return this builder for method-chaining.
085     */
086    public EventTopicsMetaDataBuilder withCoreTopicSchemaExtensions(JsonValue coreTopicSchemaExtensions) {
087        this.coreTopicSchemaExtensions = coreTopicSchemaExtensions == null ? json(object()) : coreTopicSchemaExtensions;
088        return this;
089    }
090
091    /**
092     * Specifies schemas for additional topics.
093     * <p/>
094     * Custom schema must always include _id, timestamp, transactionId and eventName fields.
095     * <p/>
096     * Example of a valid schema:
097     * <pre>
098     * "customTopic": {
099     *   "schema": {
100     *     "$schema": "http://json-schema.org/draft-04/schema#",
101     *     "id": "/",
102     *     "type": "object",
103     *     "properties": {
104     *       "_id": {
105     *         "type": "string"
106     *       },
107     *       "timestamp": {
108     *         "type": "string"
109     *       },
110     *       "transactionId": {
111     *         "type": "string"
112     *       },
113     *       "eventName": {
114     *         "type": "string"
115     *       },
116     *       "customField": {
117     *         "type": "string"
118     *       }
119     *     }
120     *   }
121     * }
122     * </pre>
123     *
124     * @param additionalTopicSchemas
125     *          the schemas of the additional event topics.
126     * @return this builder for method-chaining.
127     */
128    public EventTopicsMetaDataBuilder withAdditionalTopicSchemas(JsonValue additionalTopicSchemas) {
129        this.additionalTopicSchemas = additionalTopicSchemas == null ? json(object()) : additionalTopicSchemas;
130        return this;
131    }
132
133    /**
134     * Create a new instance of {@link EventTopicsMetaData}.
135     *
136     * @return a new instance of {@link EventTopicsMetaData}.
137     */
138    public EventTopicsMetaData build() {
139        Map<String, JsonValue> auditEventTopicSchemas = readCoreEventTopicSchemas();
140        extendCoreEventTopicsSchemas(auditEventTopicSchemas);
141        addCustomEventTopicSchemas(auditEventTopicSchemas);
142        return new EventTopicsMetaData(auditEventTopicSchemas);
143    }
144
145    private Map<String, JsonValue> readCoreEventTopicSchemas() {
146        Map<String, JsonValue> auditEvents = new HashMap<>();
147        try (final InputStream configStream = getResourceAsStream("/org/forgerock/audit/events.json")) {
148            final JsonValue predefinedEventTypes = new JsonValue(MAPPER.readValue(configStream, Map.class));
149            for (String eventTypeName : predefinedEventTypes.keys()) {
150                auditEvents.put(eventTypeName, predefinedEventTypes.get(eventTypeName));
151            }
152            return auditEvents;
153        } catch (IOException ioe) {
154            logger.error("Error while parsing core event topic schema definitions", ioe);
155            throw new RuntimeException(ioe);
156        }
157    }
158
159    private InputStream getResourceAsStream(String resourcePath) {
160        return new BufferedInputStream(getClass().getResourceAsStream(resourcePath));
161    }
162
163    private void extendCoreEventTopicsSchemas(Map<String, JsonValue> auditEventTopicSchemas) {
164        for (String topic : coreTopicSchemaExtensions.keys()) {
165            if (auditEventTopicSchemas.containsKey(topic)) {
166                JsonValue coreEventType = auditEventTopicSchemas.get(topic);
167                JsonValue coreProperties = coreEventType.get(SCHEMA).get(PROPERTIES);
168                JsonValue extendedProperties = coreTopicSchemaExtensions.get(topic).get(SCHEMA).get(PROPERTIES);
169
170                for (String property : extendedProperties.keys()) {
171                    if (coreProperties.isDefined(property)) {
172                        logger.warn("Cannot override {} property of {} topic", property, topic);
173                    } else {
174                        coreProperties.add(property, extendedProperties.get(property));
175                    }
176                }
177            }
178        }
179    }
180
181    private void addCustomEventTopicSchemas(Map<String, JsonValue> auditEventTopicSchemas) {
182        for (String topic : additionalTopicSchemas.keys()) {
183            if (!auditEventTopicSchemas.containsKey(topic)) {
184                JsonValue additionalTopicSchema = additionalTopicSchemas.get(topic);
185                if (!additionalTopicSchema.get(SCHEMA).isDefined(PROPERTIES)) {
186                    logger.warn("{} topic schema definition is invalid", topic);
187                } else {
188                    auditEventTopicSchemas.put(topic, additionalTopicSchema);
189                }
190            } else {
191                logger.warn("Cannot override pre-defined event topic {}", topic);
192            }
193        }
194    }
195
196}