Cleanup JsonExportActor
[controller.git] / opendaylight / md-sal / sal-distributed-datastore / src / main / java / org / opendaylight / controller / cluster / datastore / actors / JsonExportActor.java
1 /*
2  * Copyright (c) 2020 PANTHEON.tech, s.r.o. 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.controller.cluster.datastore.actors;
9
10 import static com.google.common.base.Preconditions.checkState;
11 import static java.util.Objects.requireNonNull;
12
13 import akka.actor.Props;
14 import com.google.gson.stream.JsonWriter;
15 import java.io.IOException;
16 import java.nio.file.Files;
17 import java.nio.file.Path;
18 import java.nio.file.Paths;
19 import java.util.ArrayList;
20 import java.util.Iterator;
21 import java.util.List;
22 import org.eclipse.jdt.annotation.NonNull;
23 import org.eclipse.jdt.annotation.Nullable;
24 import org.opendaylight.controller.cluster.common.actor.AbstractUntypedActor;
25 import org.opendaylight.controller.cluster.datastore.persisted.CommitTransactionPayload;
26 import org.opendaylight.controller.cluster.raft.ReplicatedLogEntry;
27 import org.opendaylight.controller.cluster.raft.protobuff.client.messages.Payload;
28 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
29 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
30 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNodeContainer;
31 import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeWriter;
32 import org.opendaylight.yangtools.yang.data.api.schema.tree.DataTreeCandidate;
33 import org.opendaylight.yangtools.yang.data.api.schema.tree.DataTreeCandidateNode;
34 import org.opendaylight.yangtools.yang.data.api.schema.tree.ModificationType;
35 import org.opendaylight.yangtools.yang.data.codec.gson.JSONCodecFactorySupplier;
36 import org.opendaylight.yangtools.yang.data.codec.gson.JSONNormalizedNodeStreamWriter;
37 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
38 import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack;
39
40 public final class JsonExportActor extends AbstractUntypedActor {
41     // Internal messages
42     public static final class ExportSnapshot {
43         private final String id;
44
45         private final DataTreeCandidate dataTreeCandidate;
46
47         public ExportSnapshot(final DataTreeCandidate candidate, final String id) {
48             dataTreeCandidate = requireNonNull(candidate);
49             this.id = requireNonNull(id);
50         }
51     }
52
53     public static final class ExportJournal {
54         private final ReplicatedLogEntry replicatedLogEntry;
55
56         public ExportJournal(final ReplicatedLogEntry replicatedLogEntry) {
57             this.replicatedLogEntry = requireNonNull(replicatedLogEntry);
58         }
59     }
60
61     public static final class FinishExport {
62         private final String id;
63
64         public FinishExport(final String id) {
65             this.id = requireNonNull(id);
66         }
67     }
68
69     private final List<ReplicatedLogEntry> entries = new ArrayList<>();
70     private final @NonNull EffectiveModelContext schemaContext;
71     private final @NonNull Path baseDirPath;
72
73     private JsonExportActor(final EffectiveModelContext schemaContext, final Path dirPath) {
74         this.schemaContext = requireNonNull(schemaContext);
75         baseDirPath = requireNonNull(dirPath);
76     }
77
78     public static Props props(final EffectiveModelContext schemaContext, final String dirPath) {
79         return Props.create(JsonExportActor.class, schemaContext, Paths.get(dirPath));
80     }
81
82     @Override
83     protected void handleReceive(final Object message) {
84         if (message instanceof ExportSnapshot) {
85             onExportSnapshot((ExportSnapshot) message);
86         } else if (message instanceof ExportJournal) {
87             onExportJournal((ExportJournal) message);
88         } else if (message instanceof FinishExport) {
89             onFinishExport((FinishExport)message);
90         } else {
91             unknownMessage(message);
92         }
93     }
94
95     private void onExportSnapshot(final ExportSnapshot exportSnapshot) {
96         final Path snapshotDir = baseDirPath.resolve("snapshots");
97         createDir(snapshotDir);
98
99         final Path filePath = snapshotDir.resolve(exportSnapshot.id + "-snapshot.json");
100         LOG.debug("Creating JSON file : {}", filePath);
101
102         final NormalizedNode root = exportSnapshot.dataTreeCandidate.getRootNode().getDataAfter().get();
103         checkState(root instanceof NormalizedNodeContainer, "Unexpected root %s", root);
104
105         writeSnapshot(filePath, (NormalizedNodeContainer<?>) root);
106         LOG.debug("Created JSON file: {}", filePath);
107     }
108
109     private void onExportJournal(final ExportJournal exportJournal) {
110         entries.add(exportJournal.replicatedLogEntry);
111     }
112
113     private void onFinishExport(final FinishExport finishExport) {
114         final Path journalDir = baseDirPath.resolve("journals");
115         createDir(journalDir);
116
117         final Path filePath = journalDir.resolve(finishExport.id + "-journal.json");
118         LOG.debug("Creating JSON file : {}", filePath);
119         writeJournal(filePath);
120         LOG.debug("Created JSON file: {}", filePath);
121     }
122
123     private void writeSnapshot(final Path path, final NormalizedNodeContainer<?> root) {
124         try (JsonWriter jsonWriter = new JsonWriter(Files.newBufferedWriter(path))) {
125             jsonWriter.beginObject();
126
127             try (NormalizedNodeWriter nnWriter = NormalizedNodeWriter.forStreamWriter(
128                 JSONNormalizedNodeStreamWriter.createNestedWriter(
129                     JSONCodecFactorySupplier.RFC7951.getShared(schemaContext),
130                     SchemaInferenceStack.of(schemaContext).toInference(), null, jsonWriter),
131                 true)) {
132                 for (NormalizedNode node : root.body()) {
133                     nnWriter.write(node);
134                 }
135             }
136
137             jsonWriter.endObject();
138         } catch (IOException e) {
139             LOG.error("Failed to export stapshot to {}", path, e);
140         }
141     }
142
143     private void writeJournal(final Path path) {
144         try (JsonWriter jsonWriter = new JsonWriter(Files.newBufferedWriter(path))) {
145             jsonWriter.beginObject().name("Entries");
146             jsonWriter.beginArray();
147             for (ReplicatedLogEntry entry : entries) {
148                 final Payload data = entry.getData();
149                 if (data instanceof CommitTransactionPayload) {
150                     final CommitTransactionPayload payload = (CommitTransactionPayload) entry.getData();
151                     final DataTreeCandidate candidate = payload.getCandidate().getValue().getCandidate();
152                     writeNode(jsonWriter, candidate);
153                 } else {
154                     jsonWriter.beginObject().name("Payload").value(data.toString()).endObject();
155                 }
156             }
157             jsonWriter.endArray();
158             jsonWriter.endObject();
159         } catch (IOException e) {
160             LOG.error("Failed to export journal to {}", path, e);
161         }
162     }
163
164     private static void writeNode(final JsonWriter writer, final DataTreeCandidate candidate) throws IOException {
165         writer.beginObject();
166         writer.name("Entry");
167         writer.beginArray();
168         doWriteNode(writer, candidate.getRootPath(), candidate.getRootNode());
169         writer.endArray();
170         writer.endObject();
171     }
172
173     private static void doWriteNode(final JsonWriter writer, final YangInstanceIdentifier path,
174             final DataTreeCandidateNode node) throws IOException {
175         switch (node.getModificationType()) {
176             case APPEARED:
177             case DISAPPEARED:
178             case SUBTREE_MODIFIED:
179                 NodeIterator iterator = new NodeIterator(null, path, node.getChildNodes().iterator());
180                 do {
181                     iterator = iterator.next(writer);
182                 } while (iterator != null);
183                 break;
184             case DELETE:
185             case UNMODIFIED:
186             case WRITE:
187                 outputNodeInfo(writer, path, node);
188                 break;
189             default:
190                 outputDefault(writer, path, node);
191         }
192     }
193
194     private static void outputNodeInfo(final JsonWriter writer, final YangInstanceIdentifier path,
195                                        final DataTreeCandidateNode node) throws IOException {
196         final ModificationType modificationType = node.getModificationType();
197
198         writer.beginObject().name("Node");
199         writer.beginArray();
200         writer.beginObject().name("Path").value(path.toString()).endObject();
201         writer.beginObject().name("ModificationType").value(modificationType.toString()).endObject();
202         if (modificationType == ModificationType.WRITE) {
203             writer.beginObject().name("Data").value(node.getDataAfter().get().body().toString()).endObject();
204         }
205         writer.endArray();
206         writer.endObject();
207     }
208
209     private static void outputDefault(final JsonWriter writer, final YangInstanceIdentifier path,
210                                       final DataTreeCandidateNode node) throws IOException {
211         writer.beginObject().name("Node");
212         writer.beginArray();
213         writer.beginObject().name("Path").value(path.toString()).endObject();
214         writer.beginObject().name("ModificationType")
215                 .value("UNSUPPORTED MODIFICATION: " + node.getModificationType()).endObject();
216         writer.endArray();
217         writer.endObject();
218     }
219
220     private void createDir(final Path path) {
221         try {
222             Files.createDirectories(path);
223         } catch (IOException e) {
224             LOG.warn("Directory {} cannot be created", path, e);
225         }
226     }
227
228     private static final class NodeIterator {
229         private final Iterator<DataTreeCandidateNode> iterator;
230         private final YangInstanceIdentifier path;
231         private final NodeIterator parent;
232
233         NodeIterator(final @Nullable NodeIterator parent, final YangInstanceIdentifier path,
234                      final Iterator<DataTreeCandidateNode> iterator) {
235             this.iterator = requireNonNull(iterator);
236             this.path = requireNonNull(path);
237             this.parent = parent;
238         }
239
240         NodeIterator next(final JsonWriter writer) throws IOException {
241             while (iterator.hasNext()) {
242                 final DataTreeCandidateNode node = iterator.next();
243                 final YangInstanceIdentifier child = path.node(node.getIdentifier());
244
245                 switch (node.getModificationType()) {
246                     case APPEARED:
247                     case DISAPPEARED:
248                     case SUBTREE_MODIFIED:
249                         return new NodeIterator(this, child, node.getChildNodes().iterator());
250                     case DELETE:
251                     case UNMODIFIED:
252                     case WRITE:
253                         outputNodeInfo(writer, path, node);
254                         break;
255                     default:
256                         outputDefault(writer, child, node);
257                 }
258             }
259
260             return parent;
261         }
262     }
263 }