b2cdc052af0387b23ae44f57aa7cb852d41ccb62
[netconf.git] / restconf / sal-rest-connector / src / main / java / org / opendaylight / netconf / sal / streams / listeners / ListenerAdapter.java
1 /*
2  * Copyright (c) 2014 Cisco Systems, Inc. and others.  All rights reserved.
3  *
4  * This program and the accompanying materials are made available under the
5  * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6  * and is available at http://www.eclipse.org/legal/epl-v10.html
7  */
8 package org.opendaylight.netconf.sal.streams.listeners;
9
10 import com.google.common.base.Preconditions;
11 import com.google.common.eventbus.AsyncEventBus;
12 import com.google.common.eventbus.EventBus;
13 import com.google.common.eventbus.Subscribe;
14 import io.netty.channel.Channel;
15 import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
16 import io.netty.util.internal.ConcurrentSet;
17 import java.io.ByteArrayOutputStream;
18 import java.io.IOException;
19 import java.io.OutputStreamWriter;
20 import java.io.UnsupportedEncodingException;
21 import java.nio.charset.StandardCharsets;
22 import java.text.SimpleDateFormat;
23 import java.util.Collection;
24 import java.util.Date;
25 import java.util.HashMap;
26 import java.util.Map;
27 import java.util.Map.Entry;
28 import java.util.Random;
29 import java.util.Set;
30 import java.util.concurrent.Executors;
31 import java.util.regex.Pattern;
32 import javax.xml.parsers.DocumentBuilder;
33 import javax.xml.parsers.DocumentBuilderFactory;
34 import javax.xml.parsers.ParserConfigurationException;
35 import javax.xml.stream.XMLOutputFactory;
36 import javax.xml.stream.XMLStreamException;
37 import javax.xml.stream.XMLStreamWriter;
38 import javax.xml.transform.OutputKeys;
39 import javax.xml.transform.Transformer;
40 import javax.xml.transform.TransformerException;
41 import javax.xml.transform.TransformerFactory;
42 import javax.xml.transform.dom.DOMResult;
43 import javax.xml.transform.dom.DOMSource;
44 import javax.xml.transform.stream.StreamResult;
45 import org.json.JSONObject;
46 import org.json.XML;
47 import org.opendaylight.controller.md.sal.common.api.data.AsyncDataChangeEvent;
48 import org.opendaylight.controller.md.sal.dom.api.DOMDataChangeListener;
49 import org.opendaylight.netconf.sal.restconf.impl.ControllerContext;
50 import org.opendaylight.yang.gen.v1.urn.sal.restconf.event.subscription.rev140708.NotificationOutputTypeGrouping.NotificationOutputType;
51 import org.opendaylight.yangtools.concepts.ListenerRegistration;
52 import org.opendaylight.yangtools.yang.common.QName;
53 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
54 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
55 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeWithValue;
56 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
57 import org.opendaylight.yangtools.yang.data.api.schema.MapEntryNode;
58 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
59 import org.opendaylight.yangtools.yang.data.api.schema.UnkeyedListEntryNode;
60 import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter;
61 import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeWriter;
62 import org.opendaylight.yangtools.yang.data.impl.codec.xml.XMLStreamNormalizedNodeStreamWriter;
63 import org.opendaylight.yangtools.yang.data.impl.codec.xml.XmlDocumentUtils;
64 import org.opendaylight.yangtools.yang.data.util.DataSchemaContextTree;
65 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
66 import org.opendaylight.yangtools.yang.model.api.SchemaPath;
67 import org.slf4j.Logger;
68 import org.slf4j.LoggerFactory;
69 import org.w3c.dom.Document;
70 import org.w3c.dom.Element;
71 import org.w3c.dom.Node;
72
73 /**
74  * {@link ListenerAdapter} is responsible to track events, which occurred by changing data in data source.
75  */
76 public class ListenerAdapter implements DOMDataChangeListener {
77
78     private static final Logger LOG = LoggerFactory.getLogger(ListenerAdapter.class);
79     private static final DocumentBuilderFactory DBF = DocumentBuilderFactory.newInstance();
80     private static final TransformerFactory FACTORY = TransformerFactory.newInstance();
81     private static final Pattern RFC3339_PATTERN = Pattern.compile("(\\d\\d)(\\d\\d)$");
82
83     private static final SimpleDateFormat RFC3339 = new SimpleDateFormat("yyyy-MM-dd'T'hh:mm:ssZ");
84
85     private final YangInstanceIdentifier path;
86     private ListenerRegistration<DOMDataChangeListener> registration;
87     private final String streamName;
88     private Set<Channel> subscribers = new ConcurrentSet<>();
89     private final EventBus eventBus;
90     private final EventBusChangeRecorder eventBusChangeRecorder;
91     private final NotificationOutputType outputType;
92
93     /**
94      * Creates new {@link ListenerAdapter} listener specified by path and stream
95      * name.
96      *
97      * @param path
98      *            Path to data in data store.
99      * @param streamName
100      *            The name of the stream.
101      * @param outputType
102      *            - type of output on notification (JSON, XML)
103      */
104     ListenerAdapter(final YangInstanceIdentifier path, final String streamName,
105             final NotificationOutputType outputType) {
106         this.outputType = outputType;
107         Preconditions.checkNotNull(path);
108         Preconditions.checkArgument((streamName != null) && !streamName.isEmpty());
109         this.path = path;
110         this.streamName = streamName;
111         this.eventBus = new AsyncEventBus(Executors.newSingleThreadExecutor());
112         this.eventBusChangeRecorder = new EventBusChangeRecorder();
113         this.eventBus.register(this.eventBusChangeRecorder);
114     }
115
116     @Override
117     public void onDataChanged(final AsyncDataChangeEvent<YangInstanceIdentifier, NormalizedNode<?, ?>> change) {
118         if (!change.getCreatedData().isEmpty() || !change.getUpdatedData().isEmpty()
119                 || !change.getRemovedPaths().isEmpty()) {
120             final String xml = prepareXmlFrom(change);
121             final Event event = new Event(EventType.NOTIFY);
122             if (this.outputType.equals(NotificationOutputType.JSON)) {
123                 final JSONObject jsonObject = XML.toJSONObject(xml);
124                 event.setData(jsonObject.toString());
125             } else {
126                 event.setData(xml);
127             }
128             this.eventBus.post(event);
129         }
130     }
131
132     /**
133      * Tracks events of data change by customer.
134      */
135     private final class EventBusChangeRecorder {
136         @Subscribe
137         public void recordCustomerChange(final Event event) {
138             if (event.getType() == EventType.REGISTER) {
139                 final Channel subscriber = event.getSubscriber();
140                 if (!ListenerAdapter.this.subscribers.contains(subscriber)) {
141                     ListenerAdapter.this.subscribers.add(subscriber);
142                 }
143             } else if (event.getType() == EventType.DEREGISTER) {
144                 ListenerAdapter.this.subscribers.remove(event.getSubscriber());
145                 Notificator.removeListenerIfNoSubscriberExists(ListenerAdapter.this);
146             } else if (event.getType() == EventType.NOTIFY) {
147                 for (final Channel subscriber : ListenerAdapter.this.subscribers) {
148                     if (subscriber.isActive()) {
149                         LOG.debug("Data are sent to subscriber {}:", subscriber.remoteAddress());
150                         subscriber.writeAndFlush(new TextWebSocketFrame(event.getData()));
151                     } else {
152                         LOG.debug("Subscriber {} is removed - channel is not active yet.", subscriber.remoteAddress());
153                         ListenerAdapter.this.subscribers.remove(subscriber);
154                     }
155                 }
156             }
157         }
158     }
159
160     /**
161      * Represents event of specific {@link EventType} type, holds data and {@link Channel} subscriber.
162      */
163     private final class Event {
164         private final EventType type;
165         private Channel subscriber;
166         private String data;
167
168         /**
169          * Creates new event specified by {@link EventType} type.
170          *
171          * @param type
172          *            EventType
173          */
174         public Event(final EventType type) {
175             this.type = type;
176         }
177
178         /**
179          * Gets the {@link Channel} subscriber.
180          *
181          * @return Channel
182          */
183         public Channel getSubscriber() {
184             return this.subscriber;
185         }
186
187         /**
188          * Sets subscriber for event.
189          *
190          * @param subscriber
191          *            Channel
192          */
193         public void setSubscriber(final Channel subscriber) {
194             this.subscriber = subscriber;
195         }
196
197         /**
198          * Gets event String.
199          *
200          * @return String representation of event data.
201          */
202         public String getData() {
203             return this.data;
204         }
205
206         /**
207          * Sets event data.
208          *
209          * @param data String.
210          */
211         public void setData(final String data) {
212             this.data = data;
213         }
214
215         /**
216          * Gets event type.
217          *
218          * @return The type of the event.
219          */
220         public EventType getType() {
221             return this.type;
222         }
223     }
224
225     /**
226      * Type of the event.
227      */
228     private enum EventType {
229         REGISTER,
230         DEREGISTER,
231         NOTIFY;
232     }
233
234     /**
235      * Prepare data in printable form and transform it to String.
236      *
237      * @param change
238      *            DataChangeEvent
239      * @return Data in printable form.
240      */
241     private String prepareXmlFrom(final AsyncDataChangeEvent<YangInstanceIdentifier, NormalizedNode<?, ?>> change) {
242         final SchemaContext schemaContext = ControllerContext.getInstance().getGlobalSchema();
243         final DataSchemaContextTree dataContextTree =  DataSchemaContextTree.from(schemaContext);
244         final Document doc = createDocument();
245         final Element notificationElement = doc.createElementNS("urn:ietf:params:xml:ns:netconf:notification:1.0",
246                 "notification");
247
248         doc.appendChild(notificationElement);
249
250         final Element eventTimeElement = doc.createElement("eventTime");
251         eventTimeElement.setTextContent(toRFC3339(new Date()));
252         notificationElement.appendChild(eventTimeElement);
253
254         final Element dataChangedNotificationEventElement = doc.createElementNS(
255                 "urn:opendaylight:params:xml:ns:yang:controller:md:sal:remote", "data-changed-notification");
256
257         addValuesToDataChangedNotificationEventElement(doc, dataChangedNotificationEventElement, change,
258                 schemaContext, dataContextTree);
259         notificationElement.appendChild(dataChangedNotificationEventElement);
260
261         try {
262             final ByteArrayOutputStream out = new ByteArrayOutputStream();
263             final Transformer transformer = FACTORY.newTransformer();
264             transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no");
265             transformer.setOutputProperty(OutputKeys.METHOD, "xml");
266             transformer.setOutputProperty(OutputKeys.INDENT, "yes");
267             transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
268             transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4");
269             transformer.transform(new DOMSource(doc),
270                     new StreamResult(new OutputStreamWriter(out, StandardCharsets.UTF_8)));
271             final byte[] charData = out.toByteArray();
272             return new String(charData, "UTF-8");
273         } catch (TransformerException | UnsupportedEncodingException e) {
274             final String msg = "Error during transformation of Document into String";
275             LOG.error(msg, e);
276             return msg;
277         }
278     }
279
280     /**
281      * Formats data specified by RFC3339.
282      *
283      * @param d
284      *            Date
285      * @return Data specified by RFC3339.
286      */
287     public static String toRFC3339(final Date d) {
288         return RFC3339_PATTERN.matcher(RFC3339.format(d)).replaceAll("$1:$2");
289     }
290
291     /**
292      * Creates {@link Document} document.
293      * @return {@link Document} document.
294      */
295     public static Document createDocument() {
296         final DocumentBuilder bob;
297         try {
298             bob = DBF.newDocumentBuilder();
299         } catch (final ParserConfigurationException e) {
300             return null;
301         }
302         return bob.newDocument();
303     }
304
305     /**
306      * Adds values to data changed notification event element.
307      *
308      * @param doc
309      *            {@link Document}
310      * @param dataChangedNotificationEventElement
311      *            {@link Element}
312      * @param change
313      *            {@link AsyncDataChangeEvent}
314      */
315     private void addValuesToDataChangedNotificationEventElement(final Document doc,
316             final Element dataChangedNotificationEventElement,
317             final AsyncDataChangeEvent<YangInstanceIdentifier, NormalizedNode<?, ?>> change,
318             final SchemaContext  schemaContext, final DataSchemaContextTree dataSchemaContextTree) {
319
320         addCreatedChangedValuesFromDataToElement(doc, change.getCreatedData().entrySet(),
321                 dataChangedNotificationEventElement,
322                 Operation.CREATED, schemaContext, dataSchemaContextTree);
323
324         addCreatedChangedValuesFromDataToElement(doc, change.getUpdatedData().entrySet(),
325                     dataChangedNotificationEventElement,
326                     Operation.UPDATED, schemaContext, dataSchemaContextTree);
327
328         addValuesFromDataToElement(doc, change.getRemovedPaths(), dataChangedNotificationEventElement,
329                 Operation.DELETED);
330     }
331
332     /**
333      * Adds values from data to element.
334      *
335      * @param doc
336      *            {@link Document}
337      * @param data
338      *            Set of {@link YangInstanceIdentifier}.
339      * @param element
340      *            {@link Element}
341      * @param operation
342      *            {@link Operation}
343      */
344     private void addValuesFromDataToElement(final Document doc, final Set<YangInstanceIdentifier> data,
345             final Element element, final Operation operation) {
346         if ((data == null) || data.isEmpty()) {
347             return;
348         }
349         for (final YangInstanceIdentifier path : data) {
350             if (!ControllerContext.getInstance().isNodeMixin(path)) {
351                 final Node node = createDataChangeEventElement(doc, path, operation);
352                 element.appendChild(node);
353             }
354         }
355     }
356
357     private void addCreatedChangedValuesFromDataToElement(final Document doc, final Set<Entry<YangInstanceIdentifier,
358                 NormalizedNode<?,?>>> data, final Element element, final Operation operation, final SchemaContext
359             schemaContext, final DataSchemaContextTree dataSchemaContextTree) {
360         if ((data == null) || data.isEmpty()) {
361             return;
362         }
363         for (final Entry<YangInstanceIdentifier, NormalizedNode<?, ?>> entry : data) {
364             if (!ControllerContext.getInstance().isNodeMixin(entry.getKey())) {
365                 final Node node = createCreatedChangedDataChangeEventElement(doc, entry, operation, schemaContext,
366                         dataSchemaContextTree);
367                 element.appendChild(node);
368             }
369         }
370     }
371
372     /**
373      * Creates changed event element from data.
374      *
375      * @param doc
376      *            {@link Document}
377      * @param path
378      *            Path to data in data store.
379      * @param operation
380      *            {@link Operation}
381      * @return {@link Node} node represented by changed event element.
382      */
383     private Node createDataChangeEventElement(final Document doc, final YangInstanceIdentifier path,
384             final Operation operation) {
385         final Element dataChangeEventElement = doc.createElement("data-change-event");
386         final Element pathElement = doc.createElement("path");
387         addPathAsValueToElement(path, pathElement);
388         dataChangeEventElement.appendChild(pathElement);
389
390         final Element operationElement = doc.createElement("operation");
391         operationElement.setTextContent(operation.value);
392         dataChangeEventElement.appendChild(operationElement);
393
394         return dataChangeEventElement;
395     }
396
397     private Node createCreatedChangedDataChangeEventElement(final Document doc, final Entry<YangInstanceIdentifier,
398             NormalizedNode<?, ?>> entry, final Operation operation, final SchemaContext
399             schemaContext, final DataSchemaContextTree dataSchemaContextTree) {
400         final Element dataChangeEventElement = doc.createElement("data-change-event");
401         final Element pathElement = doc.createElement("path");
402         final YangInstanceIdentifier path = entry.getKey();
403         addPathAsValueToElement(path, pathElement);
404         dataChangeEventElement.appendChild(pathElement);
405
406         final Element operationElement = doc.createElement("operation");
407         operationElement.setTextContent(operation.value);
408         dataChangeEventElement.appendChild(operationElement);
409
410         try {
411             final DOMResult domResult = writeNormalizedNode(entry.getValue(), path,
412                     schemaContext, dataSchemaContextTree);
413             final Node result = doc.importNode(domResult.getNode().getFirstChild(), true);
414             final Element dataElement = doc.createElement("data");
415             dataElement.appendChild(result);
416             dataChangeEventElement.appendChild(dataElement);
417         } catch (final IOException e) {
418             LOG.error("Error in writer ", e);
419         } catch (final XMLStreamException e) {
420             LOG.error("Error processing stream", e);
421         }
422
423         return dataChangeEventElement;
424     }
425
426     private static DOMResult writeNormalizedNode(final NormalizedNode<?, ?> normalized,
427                                                  final YangInstanceIdentifier path, final SchemaContext context,
428                                                  final DataSchemaContextTree dataSchemaContextTree)
429             throws IOException, XMLStreamException {
430         final XMLOutputFactory XML_FACTORY = XMLOutputFactory.newFactory();
431         final Document doc = XmlDocumentUtils.getDocument();
432         final DOMResult result = new DOMResult(doc);
433         NormalizedNodeWriter normalizedNodeWriter = null;
434         NormalizedNodeStreamWriter normalizedNodeStreamWriter = null;
435         XMLStreamWriter writer = null;
436         final SchemaPath nodePath;
437
438         if ((normalized instanceof MapEntryNode) || (normalized instanceof UnkeyedListEntryNode)) {
439             nodePath = dataSchemaContextTree.getChild(path).getDataSchemaNode().getPath();
440         } else {
441             nodePath = dataSchemaContextTree.getChild(path).getDataSchemaNode().getPath().getParent();
442         }
443
444         try {
445             writer = XML_FACTORY.createXMLStreamWriter(result);
446             normalizedNodeStreamWriter = XMLStreamNormalizedNodeStreamWriter.create(writer, context, nodePath);
447             normalizedNodeWriter = NormalizedNodeWriter.forStreamWriter(normalizedNodeStreamWriter);
448
449             normalizedNodeWriter.write(normalized);
450
451             normalizedNodeWriter.flush();
452         } finally {
453             if (normalizedNodeWriter != null) {
454                 normalizedNodeWriter.close();
455             }
456             if (normalizedNodeStreamWriter != null) {
457                 normalizedNodeStreamWriter.close();
458             }
459             if (writer != null) {
460                 writer.close();
461             }
462         }
463
464         return result;
465     }
466
467     /**
468      * Adds path as value to element.
469      *
470      * @param path
471      *            Path to data in data store.
472      * @param element
473      *            {@link Element}
474      */
475     private void addPathAsValueToElement(final YangInstanceIdentifier path, final Element element) {
476         // Map< key = namespace, value = prefix>
477         final Map<String, String> prefixes = new HashMap<>();
478         final YangInstanceIdentifier normalizedPath = ControllerContext.getInstance().toXpathRepresentation(path);
479         final StringBuilder textContent = new StringBuilder();
480
481         // FIXME: BUG-1281: this is duplicated code from yangtools (BUG-1275)
482         for (final PathArgument pathArgument : normalizedPath.getPathArguments()) {
483             if (pathArgument instanceof YangInstanceIdentifier.AugmentationIdentifier) {
484                 continue;
485             }
486             textContent.append("/");
487             writeIdentifierWithNamespacePrefix(element, textContent, pathArgument.getNodeType(), prefixes);
488             if (pathArgument instanceof NodeIdentifierWithPredicates) {
489                 final Map<QName, Object> predicates = ((NodeIdentifierWithPredicates) pathArgument).getKeyValues();
490                 for (final QName keyValue : predicates.keySet()) {
491                     final String predicateValue = String.valueOf(predicates.get(keyValue));
492                     textContent.append("[");
493                     writeIdentifierWithNamespacePrefix(element, textContent, keyValue, prefixes);
494                     textContent.append("='");
495                     textContent.append(predicateValue);
496                     textContent.append("'");
497                     textContent.append("]");
498                 }
499             } else if (pathArgument instanceof NodeWithValue) {
500                 textContent.append("[.='");
501                 textContent.append(((NodeWithValue) pathArgument).getValue());
502                 textContent.append("'");
503                 textContent.append("]");
504             }
505         }
506         element.setTextContent(textContent.toString());
507     }
508
509     /**
510      * Writes identifier that consists of prefix and QName.
511      *
512      * @param element
513      *            {@link Element}
514      * @param textContent
515      *            StringBuilder
516      * @param qName
517      *            QName
518      * @param prefixes
519      *            Map of namespaces and prefixes.
520      */
521     private static void writeIdentifierWithNamespacePrefix(final Element element, final StringBuilder textContent,
522             final QName qName, final Map<String, String> prefixes) {
523         final String namespace = qName.getNamespace().toString();
524         String prefix = prefixes.get(namespace);
525         if (prefix == null) {
526             prefix = generateNewPrefix(prefixes.values());
527         }
528
529         element.setAttribute("xmlns:" + prefix, namespace);
530         textContent.append(prefix);
531         prefixes.put(namespace, prefix);
532
533         textContent.append(":");
534         textContent.append(qName.getLocalName());
535     }
536
537     /**
538      * Generates new prefix which consists of four random characters <a-z>.
539      *
540      * @param prefixes
541      *            Collection of prefixes.
542      * @return New prefix which consists of four random characters <a-z>.
543      */
544     private static String generateNewPrefix(final Collection<String> prefixes) {
545         StringBuilder result = null;
546         final Random random = new Random();
547         do {
548             result = new StringBuilder();
549             for (int i = 0; i < 4; i++) {
550                 final int randomNumber = 0x61 + (Math.abs(random.nextInt()) % 26);
551                 result.append(Character.toChars(randomNumber));
552             }
553         } while (prefixes.contains(result.toString()));
554
555         return result.toString();
556     }
557
558     /**
559      * Gets path pointed to data in data store.
560      *
561      * @return Path pointed to data in data store.
562      */
563     public YangInstanceIdentifier getPath() {
564         return this.path;
565     }
566
567     /**
568      * Sets {@link ListenerRegistration} registration.
569      *
570      * @param registration DOMDataChangeListener registration
571      */
572     public void setRegistration(final ListenerRegistration<DOMDataChangeListener> registration) {
573         this.registration = registration;
574     }
575
576     /**
577      * Gets the name of the stream.
578      *
579      * @return The name of the stream.
580      */
581     public String getStreamName() {
582         return this.streamName;
583     }
584
585     /**
586      * Removes all subscribers and unregisters event bus change recorder form event bus.
587      */
588     public void close() throws Exception {
589         this.subscribers = new ConcurrentSet<>();
590         this.registration.close();
591         this.registration = null;
592         this.eventBus.unregister(this.eventBusChangeRecorder);
593     }
594
595     /**
596      * Checks if {@link ListenerRegistration} registration exist.
597      *
598      * @return True if exist, false otherwise.
599      */
600     public boolean isListening() {
601         return this.registration == null ? false : true;
602     }
603
604     /**
605      * Creates event of type {@link EventType#REGISTER}, set {@link Channel} subscriber to the event and post event into
606      * event bus.
607      *
608      * @param subscriber
609      *            Channel
610      */
611     public void addSubscriber(final Channel subscriber) {
612         if (!subscriber.isActive()) {
613             LOG.debug("Channel is not active between websocket server and subscriber {}" + subscriber.remoteAddress());
614         }
615         final Event event = new Event(EventType.REGISTER);
616         event.setSubscriber(subscriber);
617         this.eventBus.post(event);
618     }
619
620     /**
621      * Creates event of type {@link EventType#DEREGISTER}, sets {@link Channel} subscriber to the event and posts event
622      * into event bus.
623      *
624      * @param subscriber
625      */
626     public void removeSubscriber(final Channel subscriber) {
627         LOG.debug("Subscriber {} is removed.", subscriber.remoteAddress());
628         final Event event = new Event(EventType.DEREGISTER);
629         event.setSubscriber(subscriber);
630         this.eventBus.post(event);
631     }
632
633     /**
634      * Checks if exists at least one {@link Channel} subscriber.
635      *
636      * @return True if exist at least one {@link Channel} subscriber, false otherwise.
637      */
638     public boolean hasSubscribers() {
639         return !this.subscribers.isEmpty();
640     }
641
642     /**
643      * Consists of two types {@link Store#CONFIG} and {@link Store#OPERATION}.
644      */
645     private static enum Store {
646         CONFIG("config"),
647         OPERATION("operation");
648
649         private final String value;
650
651         private Store(final String value) {
652             this.value = value;
653         }
654     }
655
656     /**
657      * Consists of three types {@link Operation#CREATED}, {@link Operation#UPDATED} and {@link Operation#DELETED}.
658      */
659     private static enum Operation {
660         CREATED("created"),
661         UPDATED("updated"),
662         DELETED("deleted");
663
664         private final String value;
665
666         private Operation(final String value) {
667             this.value = value;
668         }
669     }
670
671 }