blob: 41167ed223d0921e4dd89e64a84d62786b26cfd4 [file] [log] [blame]
Thomas Vachuska96d55b12015-05-11 08:52:03 -07001/*
Brian O'Connor5ab426f2016-04-09 01:19:45 -07002 * Copyright 2015-present Open Networking Laboratory
Thomas Vachuska96d55b12015-05-11 08:52:03 -07003 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
Thomas Vachuska4998caa2015-08-26 13:28:38 -070016package org.onosproject.store.config.impl;
Thomas Vachuska96d55b12015-05-11 08:52:03 -070017
Thomas Vachuska0a400ea2015-09-04 11:25:03 -070018import com.fasterxml.jackson.databind.JsonNode;
Thomas Vachuska96d55b12015-05-11 08:52:03 -070019import com.fasterxml.jackson.databind.ObjectMapper;
Jonathan Hart111b42b2015-07-14 13:28:05 -070020import com.fasterxml.jackson.databind.node.ArrayNode;
Thomas Vachuska96d55b12015-05-11 08:52:03 -070021import com.fasterxml.jackson.databind.node.BooleanNode;
22import com.fasterxml.jackson.databind.node.DoubleNode;
Ayaka Koshibe1a002512015-09-03 13:09:23 -070023import com.fasterxml.jackson.databind.node.IntNode;
Thomas Vachuska96d55b12015-05-11 08:52:03 -070024import com.fasterxml.jackson.databind.node.JsonNodeFactory;
25import com.fasterxml.jackson.databind.node.LongNode;
HIGUCHI Yutad9fe3a32015-11-24 18:52:25 -080026import com.fasterxml.jackson.databind.node.NullNode;
Thomas Vachuska96d55b12015-05-11 08:52:03 -070027import com.fasterxml.jackson.databind.node.ObjectNode;
28import com.fasterxml.jackson.databind.node.ShortNode;
29import com.fasterxml.jackson.databind.node.TextNode;
30import com.google.common.collect.ImmutableSet;
31import com.google.common.collect.Maps;
32import org.apache.felix.scr.annotations.Activate;
33import org.apache.felix.scr.annotations.Component;
34import org.apache.felix.scr.annotations.Deactivate;
35import org.apache.felix.scr.annotations.Reference;
36import org.apache.felix.scr.annotations.ReferenceCardinality;
37import org.apache.felix.scr.annotations.Service;
38import org.onlab.util.KryoNamespace;
Thomas Vachuska5f2cbe62015-07-30 15:10:34 -070039import org.onlab.util.Tools;
Ray Milkeya4122362015-08-18 15:19:08 -070040import org.onosproject.net.config.Config;
41import org.onosproject.net.config.ConfigApplyDelegate;
42import org.onosproject.net.config.ConfigFactory;
43import org.onosproject.net.config.NetworkConfigEvent;
44import org.onosproject.net.config.NetworkConfigStore;
45import org.onosproject.net.config.NetworkConfigStoreDelegate;
Thomas Vachuska96d55b12015-05-11 08:52:03 -070046import org.onosproject.store.AbstractStore;
47import org.onosproject.store.serializers.KryoNamespaces;
48import org.onosproject.store.service.ConsistentMap;
Thomas Vachuska5f2cbe62015-07-30 15:10:34 -070049import org.onosproject.store.service.ConsistentMapException;
Thomas Vachuska96d55b12015-05-11 08:52:03 -070050import org.onosproject.store.service.MapEvent;
51import org.onosproject.store.service.MapEventListener;
52import org.onosproject.store.service.Serializer;
53import org.onosproject.store.service.StorageService;
54import org.onosproject.store.service.Versioned;
55import org.slf4j.Logger;
56import org.slf4j.LoggerFactory;
57
58import java.util.LinkedHashMap;
59import java.util.Map;
60import java.util.Objects;
61import java.util.Set;
62
Thomas Vachuskace0bbb32015-11-18 16:56:10 -080063import static com.google.common.base.Preconditions.checkArgument;
Jonathan Hartb11c4d02016-03-23 09:05:44 -070064import static org.onosproject.net.config.NetworkConfigEvent.Type.CONFIG_ADDED;
65import static org.onosproject.net.config.NetworkConfigEvent.Type.CONFIG_REGISTERED;
66import static org.onosproject.net.config.NetworkConfigEvent.Type.CONFIG_REMOVED;
67import static org.onosproject.net.config.NetworkConfigEvent.Type.CONFIG_UNREGISTERED;
68import static org.onosproject.net.config.NetworkConfigEvent.Type.CONFIG_UPDATED;
Thomas Vachuska96d55b12015-05-11 08:52:03 -070069
70/**
71 * Implementation of a distributed network configuration store.
72 */
73@Component(immediate = true)
74@Service
75public class DistributedNetworkConfigStore
76 extends AbstractStore<NetworkConfigEvent, NetworkConfigStoreDelegate>
77 implements NetworkConfigStore {
78
79 private final Logger log = LoggerFactory.getLogger(getClass());
80
Thomas Vachuskace0bbb32015-11-18 16:56:10 -080081 private static final int MAX_BACKOFF = 10;
82 private static final String INVALID_CONFIG_JSON =
83 "JSON node does not contain valid configuration";
Jonathan Hartb11c4d02016-03-23 09:05:44 -070084 private static final String INVALID_JSON_LIST =
85 "JSON node is not a list for list type config";
86 private static final String INVALID_JSON_OBJECT =
87 "JSON node is not an object for object type config";
Thomas Vachuskace0bbb32015-11-18 16:56:10 -080088
Thomas Vachuska96d55b12015-05-11 08:52:03 -070089 @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
90 protected StorageService storageService;
91
Thomas Vachuska0a400ea2015-09-04 11:25:03 -070092 private ConsistentMap<ConfigKey, JsonNode> configs;
Thomas Vachuska96d55b12015-05-11 08:52:03 -070093
94 private final Map<String, ConfigFactory> factoriesByConfig = Maps.newConcurrentMap();
95 private final ObjectMapper mapper = new ObjectMapper();
96 private final ConfigApplyDelegate applyDelegate = new InternalApplyDelegate();
Thomas Vachuska0a400ea2015-09-04 11:25:03 -070097 private final MapEventListener<ConfigKey, JsonNode> listener = new InternalMapListener();
Thomas Vachuska96d55b12015-05-11 08:52:03 -070098
99 @Activate
100 public void activate() {
101 KryoNamespace.Builder kryoBuilder = new KryoNamespace.Builder()
102 .register(KryoNamespaces.API)
Jonathan Hart111b42b2015-07-14 13:28:05 -0700103 .register(ConfigKey.class, ObjectNode.class, ArrayNode.class,
Thomas Vachuska96d55b12015-05-11 08:52:03 -0700104 JsonNodeFactory.class, LinkedHashMap.class,
105 TextNode.class, BooleanNode.class,
HIGUCHI Yutad9fe3a32015-11-24 18:52:25 -0800106 LongNode.class, DoubleNode.class, ShortNode.class, IntNode.class,
107 NullNode.class);
Thomas Vachuska96d55b12015-05-11 08:52:03 -0700108
Thomas Vachuska0a400ea2015-09-04 11:25:03 -0700109 configs = storageService.<ConfigKey, JsonNode>consistentMapBuilder()
Thomas Vachuska96d55b12015-05-11 08:52:03 -0700110 .withSerializer(Serializer.using(kryoBuilder.build()))
111 .withName("onos-network-configs")
Madan Jampani3d6a2f62015-08-12 07:19:07 -0700112 .withRelaxedReadConsistency()
Thomas Vachuska96d55b12015-05-11 08:52:03 -0700113 .build();
114 configs.addListener(listener);
115 log.info("Started");
116 }
117
118 @Deactivate
119 public void deactivate() {
120 configs.removeListener(listener);
121 log.info("Stopped");
122 }
123
124 @Override
125 public void addConfigFactory(ConfigFactory configFactory) {
126 factoriesByConfig.put(configFactory.configClass().getName(), configFactory);
Thomas Vachuska6f350ed2016-01-08 09:53:03 -0800127 processPendingConfigs(configFactory);
Thomas Vachuskae6360222015-07-21 10:10:36 -0700128 notifyDelegate(new NetworkConfigEvent(CONFIG_REGISTERED, configFactory.configKey(),
129 configFactory.configClass()));
Thomas Vachuska96d55b12015-05-11 08:52:03 -0700130 }
131
Thomas Vachuska6f350ed2016-01-08 09:53:03 -0800132 // Sweep through any pending configurations, validate them and then prune them.
133 private void processPendingConfigs(ConfigFactory configFactory) {
Thomas Vachuska096bcc82016-03-07 21:30:29 -0800134 ImmutableSet.copyOf(configs.keySet()).forEach(k -> {
135 if (Objects.equals(k.configKey, configFactory.configKey()) &&
136 isAssignableFrom(configFactory, k)) {
Thomas Vachuska6f350ed2016-01-08 09:53:03 -0800137 validateConfig(k, configFactory, configs.get(k).value());
Thomas Vachuska096bcc82016-03-07 21:30:29 -0800138 configs.remove(k); // Prune whether valid or not
Thomas Vachuska6f350ed2016-01-08 09:53:03 -0800139 }
140 });
Thomas Vachuska096bcc82016-03-07 21:30:29 -0800141 }
142
143 @SuppressWarnings("unchecked")
144 private boolean isAssignableFrom(ConfigFactory configFactory, ConfigKey k) {
145 return configFactory.subjectFactory().subjectClass().isAssignableFrom(k.subject.getClass());
Thomas Vachuska6f350ed2016-01-08 09:53:03 -0800146 }
147
148 @SuppressWarnings("unchecked")
149 private void validateConfig(ConfigKey key, ConfigFactory configFactory, JsonNode json) {
HIGUCHI Yutaca2208d2016-02-18 15:03:08 -0800150 Object subject;
151 if (key.subject instanceof String) {
152 subject = configFactory.subjectFactory().createSubject((String) key.subject);
153 } else {
154 subject = key.subject;
155 }
156 Config config = createConfig(subject, configFactory.configClass(), json);
Thomas Vachuska6f350ed2016-01-08 09:53:03 -0800157 try {
158 checkArgument(config.isValid(), INVALID_CONFIG_JSON);
HIGUCHI Yutaca2208d2016-02-18 15:03:08 -0800159 configs.putAndGet(key(subject, configFactory.configClass()), json);
Thomas Vachuska6f350ed2016-01-08 09:53:03 -0800160 } catch (Exception e) {
161 log.warn("Failed to validate pending {} configuration for {}: {}",
HIGUCHI Yutaca2208d2016-02-18 15:03:08 -0800162 key.configKey, key.subject, json);
Thomas Vachuska6f350ed2016-01-08 09:53:03 -0800163 }
164 }
165
Thomas Vachuska96d55b12015-05-11 08:52:03 -0700166 @Override
167 public void removeConfigFactory(ConfigFactory configFactory) {
168 factoriesByConfig.remove(configFactory.configClass().getName());
Thomas Vachuskae6360222015-07-21 10:10:36 -0700169 notifyDelegate(new NetworkConfigEvent(CONFIG_UNREGISTERED, configFactory.configKey(),
170 configFactory.configClass()));
Thomas Vachuska96d55b12015-05-11 08:52:03 -0700171 }
172
173 @Override
174 @SuppressWarnings("unchecked")
175 public <S, C extends Config<S>> ConfigFactory<S, C> getConfigFactory(Class<C> configClass) {
HIGUCHI Yutaca2208d2016-02-18 15:03:08 -0800176 return factoriesByConfig.get(configClass.getName());
Thomas Vachuska96d55b12015-05-11 08:52:03 -0700177 }
178
179 @Override
180 @SuppressWarnings("unchecked")
181 public <S> Set<S> getSubjects(Class<S> subjectClass) {
182 ImmutableSet.Builder<S> builder = ImmutableSet.builder();
183 configs.keySet().forEach(k -> {
184 if (subjectClass.isInstance(k.subject)) {
185 builder.add((S) k.subject);
186 }
187 });
188 return builder.build();
189 }
190
191 @Override
192 @SuppressWarnings("unchecked")
193 public <S, C extends Config<S>> Set<S> getSubjects(Class<S> subjectClass, Class<C> configClass) {
194 ImmutableSet.Builder<S> builder = ImmutableSet.builder();
195 String cName = configClass.getName();
196 configs.keySet().forEach(k -> {
Thomas Vachuska6f350ed2016-01-08 09:53:03 -0800197 if (subjectClass.isInstance(k.subject) && Objects.equals(cName, k.configClass)) {
Thomas Vachuska96d55b12015-05-11 08:52:03 -0700198 builder.add((S) k.subject);
199 }
200 });
201 return builder.build();
202 }
203
204 @Override
205 @SuppressWarnings("unchecked")
206 public <S> Set<Class<? extends Config<S>>> getConfigClasses(S subject) {
207 ImmutableSet.Builder<Class<? extends Config<S>>> builder = ImmutableSet.builder();
208 configs.keySet().forEach(k -> {
Thomas Vachuska6f350ed2016-01-08 09:53:03 -0800209 if (Objects.equals(subject, k.subject) && k.configClass != null && delegate != null) {
Thomas Vachuska96d55b12015-05-11 08:52:03 -0700210 builder.add(factoriesByConfig.get(k.configClass).configClass());
211 }
212 });
213 return builder.build();
214 }
215
216 @Override
217 public <S, T extends Config<S>> T getConfig(S subject, Class<T> configClass) {
Madan Jampania29c6772015-08-17 13:17:07 -0700218 // TODO: need to identify and address the root cause for timeouts.
Thomas Vachuska0a400ea2015-09-04 11:25:03 -0700219 Versioned<JsonNode> json = Tools.retryable(configs::get, ConsistentMapException.class, 1, MAX_BACKOFF)
220 .apply(key(subject, configClass));
Thomas Vachuska96d55b12015-05-11 08:52:03 -0700221 return json != null ? createConfig(subject, configClass, json.value()) : null;
222 }
223
224
225 @Override
226 public <S, C extends Config<S>> C createConfig(S subject, Class<C> configClass) {
Thomas Vachuska0a400ea2015-09-04 11:25:03 -0700227 ConfigFactory<S, C> factory = getConfigFactory(configClass);
228 Versioned<JsonNode> json = configs.computeIfAbsent(key(subject, configClass),
229 k -> factory.isList() ?
230 mapper.createArrayNode() :
231 mapper.createObjectNode());
Thomas Vachuska96d55b12015-05-11 08:52:03 -0700232 return createConfig(subject, configClass, json.value());
233 }
234
235 @Override
Thomas Vachuska0a400ea2015-09-04 11:25:03 -0700236 public <S, C extends Config<S>> C applyConfig(S subject, Class<C> configClass, JsonNode json) {
Thomas Vachuskace0bbb32015-11-18 16:56:10 -0800237 // Create the configuration and validate it.
238 C config = createConfig(subject, configClass, json);
239 checkArgument(config.isValid(), INVALID_CONFIG_JSON);
240
241 // Insert the validated configuration and get it back.
242 Versioned<JsonNode> versioned = configs.putAndGet(key(subject, configClass), json);
243
244 // Re-create the config if for some reason what we attempted to put
245 // was supplanted by someone else already.
Thomas Vachuska6f350ed2016-01-08 09:53:03 -0800246 return versioned.value() == json ? config : createConfig(subject, configClass, versioned.value());
247 }
248
249 @Override
250 public <S> void queueConfig(S subject, String configKey, JsonNode json) {
251 configs.put(key(subject, configKey), json);
Thomas Vachuska96d55b12015-05-11 08:52:03 -0700252 }
253
254 @Override
255 public <S, C extends Config<S>> void clearConfig(S subject, Class<C> configClass) {
256 configs.remove(key(subject, configClass));
257 }
258
Thomas Vachuska6f350ed2016-01-08 09:53:03 -0800259 @Override
260 public <S> void clearQueuedConfig(S subject, String configKey) {
261 configs.remove(key(subject, configKey));
262 }
263
Thomas Vachuska96d55b12015-05-11 08:52:03 -0700264 /**
265 * Produces a config from the specified subject, config class and raw JSON.
266 *
267 * @param subject config subject
268 * @param configClass config class
269 * @param json raw JSON data
270 * @return config object or null of no factory found or if the specified
271 * JSON is null
272 */
Thomas Vachuska96d55b12015-05-11 08:52:03 -0700273 private <S, C extends Config<S>> C createConfig(S subject, Class<C> configClass,
Thomas Vachuska0a400ea2015-09-04 11:25:03 -0700274 JsonNode json) {
Jonathan Hartb11c4d02016-03-23 09:05:44 -0700275 return createConfig(subject, configClass, json, false);
276 }
277
278 /**
279 * Produces a config from the specified subject, config class and raw JSON.
280 *
281 * The config can optionally be detached, which means it does not contain a
282 * reference to an apply delegate. This means a detached config can not be
283 * applied. This should be used only for passing the config object in the
284 * NetworkConfigEvent.
285 *
286 * @param subject config subject
287 * @param configClass config class
288 * @param json raw JSON data
289 * @param detached whether the config should be detached, that is, should
290 * be created without setting an apply delegate.
291 * @return config object or null of no factory found or if the specified
292 * JSON is null
293 */
294 @SuppressWarnings("unchecked")
295 private <S, C extends Config<S>> C createConfig(S subject, Class<C> configClass,
296 JsonNode json, boolean detached) {
Thomas Vachuska96d55b12015-05-11 08:52:03 -0700297 if (json != null) {
298 ConfigFactory<S, C> factory = factoriesByConfig.get(configClass.getName());
299 if (factory != null) {
Jonathan Hartb11c4d02016-03-23 09:05:44 -0700300 validateJsonType(json, factory);
Thomas Vachuska96d55b12015-05-11 08:52:03 -0700301 C config = factory.createConfig();
Jonathan Hartb11c4d02016-03-23 09:05:44 -0700302 config.init(subject, factory.configKey(), json, mapper,
303 detached ? null : applyDelegate);
Thomas Vachuska96d55b12015-05-11 08:52:03 -0700304 return config;
305 }
306 }
307 return null;
308 }
309
Charles Chan023a8982016-02-04 11:00:41 -0800310 /**
Jonathan Hartb11c4d02016-03-23 09:05:44 -0700311 * Validates that the type of the JSON node is appropriate for the type of
312 * configuration. A list type configuration must be created with an
313 * ArrayNode, and an object type configuration must be created with an
314 * ObjectNode.
Charles Chan023a8982016-02-04 11:00:41 -0800315 *
Jonathan Hartb11c4d02016-03-23 09:05:44 -0700316 * @param json JSON node to check
317 * @param factory config factory of configuration
318 * @param <S> subject
319 * @param <C> configuration
320 * @return true if the JSON node type is appropriate for the configuration
Charles Chan023a8982016-02-04 11:00:41 -0800321 */
Jonathan Hartb11c4d02016-03-23 09:05:44 -0700322 private <S, C extends Config<S>> boolean validateJsonType(JsonNode json,
323 ConfigFactory<S, C> factory) {
324 if (factory.isList() && !(json instanceof ArrayNode)) {
325 throw new IllegalArgumentException(INVALID_JSON_LIST);
Charles Chan023a8982016-02-04 11:00:41 -0800326 }
Jonathan Hartb11c4d02016-03-23 09:05:44 -0700327 if (!factory.isList() && !(json instanceof ObjectNode)) {
328 throw new IllegalArgumentException(INVALID_JSON_OBJECT);
329 }
330
331 return true;
Charles Chan023a8982016-02-04 11:00:41 -0800332 }
333
Thomas Vachuska96d55b12015-05-11 08:52:03 -0700334
335 // Auxiliary delegate to receive notifications about changes applied to
336 // the network configuration - by the apps.
337 private class InternalApplyDelegate implements ConfigApplyDelegate {
338 @Override
339 public void onApply(Config config) {
340 configs.put(key(config.subject(), config.getClass()), config.node());
341 }
342 }
343
344 // Produces a key for uniquely tracking a subject config.
345 private static ConfigKey key(Object subject, Class<?> configClass) {
346 return new ConfigKey(subject, configClass);
347 }
348
Thomas Vachuska6f350ed2016-01-08 09:53:03 -0800349 // Produces a key for uniquely tracking a subject config.
350 private static ConfigKey key(Object subject, String configKey) {
351 return new ConfigKey(subject, configKey);
352 }
353
Thomas Vachuska96d55b12015-05-11 08:52:03 -0700354 // Auxiliary key to track subject configurations.
Thomas Vachuska6f350ed2016-01-08 09:53:03 -0800355 // Keys with non-null configKey are pending configurations.
Thomas Vachuska96d55b12015-05-11 08:52:03 -0700356 private static final class ConfigKey {
357 final Object subject;
Thomas Vachuska6f350ed2016-01-08 09:53:03 -0800358 final String configKey;
Thomas Vachuska96d55b12015-05-11 08:52:03 -0700359 final String configClass;
360
Thomas Vachuska6f350ed2016-01-08 09:53:03 -0800361 // Create a key for pending configuration class
362 private ConfigKey(Object subject, String configKey) {
363 this.subject = subject;
364 this.configKey = configKey;
365 this.configClass = null;
366 }
367
368 // Create a key for registered class configuration
Thomas Vachuska96d55b12015-05-11 08:52:03 -0700369 private ConfigKey(Object subject, Class<?> configClass) {
370 this.subject = subject;
Thomas Vachuska6f350ed2016-01-08 09:53:03 -0800371 this.configKey = null;
Thomas Vachuska96d55b12015-05-11 08:52:03 -0700372 this.configClass = configClass.getName();
373 }
374
375 @Override
376 public int hashCode() {
Thomas Vachuska6f350ed2016-01-08 09:53:03 -0800377 return Objects.hash(subject, configKey, configClass);
Thomas Vachuska96d55b12015-05-11 08:52:03 -0700378 }
379
380 @Override
381 public boolean equals(Object obj) {
382 if (this == obj) {
383 return true;
384 }
385 if (obj instanceof ConfigKey) {
386 final ConfigKey other = (ConfigKey) obj;
387 return Objects.equals(this.subject, other.subject)
Thomas Vachuska6f350ed2016-01-08 09:53:03 -0800388 && Objects.equals(this.configKey, other.configKey)
Thomas Vachuska96d55b12015-05-11 08:52:03 -0700389 && Objects.equals(this.configClass, other.configClass);
390 }
391 return false;
392 }
393 }
394
Thomas Vachuska0a400ea2015-09-04 11:25:03 -0700395 private class InternalMapListener implements MapEventListener<ConfigKey, JsonNode> {
Thomas Vachuska96d55b12015-05-11 08:52:03 -0700396 @Override
Thomas Vachuska0a400ea2015-09-04 11:25:03 -0700397 public void event(MapEvent<ConfigKey, JsonNode> event) {
Thomas Vachuska6f350ed2016-01-08 09:53:03 -0800398 // Do not delegate pending configs.
399 if (event.key().configClass == null) {
400 return;
401 }
402
Thomas Vachuska96d55b12015-05-11 08:52:03 -0700403 ConfigFactory factory = factoriesByConfig.get(event.key().configClass);
404 if (factory != null) {
Charles Chan023a8982016-02-04 11:00:41 -0800405 Object subject = event.key().subject;
406 Class configClass = factory.configClass();
407 Versioned<JsonNode> newValue = event.newValue();
408 Versioned<JsonNode> oldValue = event.oldValue();
409
410 Config config = (newValue != null) ?
Jonathan Hartb11c4d02016-03-23 09:05:44 -0700411 createConfig(subject, configClass, newValue.value(), true) :
412 null;
Charles Chan023a8982016-02-04 11:00:41 -0800413 Config prevConfig = (oldValue != null) ?
Jonathan Hartb11c4d02016-03-23 09:05:44 -0700414 createConfig(subject, configClass, oldValue.value(), true) :
415 null;
Charles Chan023a8982016-02-04 11:00:41 -0800416
417 NetworkConfigEvent.Type type;
418 switch (event.type()) {
419 case INSERT:
420 type = CONFIG_ADDED;
421 break;
422 case UPDATE:
423 type = CONFIG_UPDATED;
424 break;
425 case REMOVE:
426 default:
427 type = CONFIG_REMOVED;
428 break;
429 }
Thomas Vachuska96d55b12015-05-11 08:52:03 -0700430 notifyDelegate(new NetworkConfigEvent(type, event.key().subject,
Charles Chan023a8982016-02-04 11:00:41 -0800431 config, prevConfig, factory.configClass()));
Thomas Vachuska96d55b12015-05-11 08:52:03 -0700432 }
433 }
434 }
435}