blob: 15332d40d9d76ece3f1448d892c13aa70a4b47b7 [file] [log] [blame]
Simon Huntd5b96732016-07-08 13:22:27 -07001/*
2 * Copyright 2016-present Open Networking Laboratory
3 *
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 */
16
17package org.onosproject.ui.impl.topo;
18
Simon Huntc13082f2016-08-03 21:20:23 -070019import com.fasterxml.jackson.databind.JsonNode;
Simon Huntd5b96732016-07-08 13:22:27 -070020import com.fasterxml.jackson.databind.ObjectMapper;
21import com.fasterxml.jackson.databind.node.ArrayNode;
22import com.fasterxml.jackson.databind.node.ObjectNode;
23import org.onlab.osgi.ServiceDirectory;
Steven Burrows583f4be2016-11-04 14:06:50 +010024import org.onlab.packet.IpAddress;
Simon Huntd5b96732016-07-08 13:22:27 -070025import org.onosproject.cluster.ClusterService;
26import org.onosproject.cluster.NodeId;
27import org.onosproject.incubator.net.PortStatisticsService;
28import org.onosproject.incubator.net.tunnel.TunnelService;
29import org.onosproject.mastership.MastershipService;
Simon Hunt6a8cb4f2016-08-09 15:08:57 -070030import org.onosproject.net.Annotated;
31import org.onosproject.net.Annotations;
32import org.onosproject.net.Device;
Steven Burrowsad75aa22016-12-14 17:17:24 -050033import org.onosproject.net.DeviceId;
Steven Burrows583f4be2016-11-04 14:06:50 +010034import org.onosproject.net.Host;
Simon Huntd5b96732016-07-08 13:22:27 -070035import org.onosproject.net.device.DeviceService;
36import org.onosproject.net.flow.FlowRuleService;
37import org.onosproject.net.host.HostService;
38import org.onosproject.net.intent.IntentService;
39import org.onosproject.net.link.LinkService;
Simon Hunt53612212016-12-04 17:19:52 -080040import org.onosproject.net.region.Region;
Simon Huntd5b96732016-07-08 13:22:27 -070041import org.onosproject.net.statistic.StatisticService;
42import org.onosproject.net.topology.TopologyService;
Steven Burrowse7cc3082016-09-27 11:24:58 -070043import org.onosproject.ui.JsonUtils;
Simon Huntd5b96732016-07-08 13:22:27 -070044import org.onosproject.ui.model.topo.UiClusterMember;
45import org.onosproject.ui.model.topo.UiDevice;
46import org.onosproject.ui.model.topo.UiHost;
47import org.onosproject.ui.model.topo.UiLink;
Simon Hunt977aa052016-07-20 17:08:29 -070048import org.onosproject.ui.model.topo.UiNode;
Simon Huntd5b96732016-07-08 13:22:27 -070049import org.onosproject.ui.model.topo.UiRegion;
Simon Huntc13082f2016-08-03 21:20:23 -070050import org.onosproject.ui.model.topo.UiSynthLink;
Simon Huntd5b96732016-07-08 13:22:27 -070051import org.onosproject.ui.model.topo.UiTopoLayout;
Simon Hunt98189192016-07-29 19:02:27 -070052import org.slf4j.Logger;
53import org.slf4j.LoggerFactory;
Simon Huntd5b96732016-07-08 13:22:27 -070054
Simon Hunt977aa052016-07-20 17:08:29 -070055import java.util.ArrayList;
56import java.util.HashMap;
57import java.util.HashSet;
Simon Huntd5b96732016-07-08 13:22:27 -070058import java.util.List;
Simon Hunt977aa052016-07-20 17:08:29 -070059import java.util.Map;
60import java.util.Set;
Simon Hunt6a8cb4f2016-08-09 15:08:57 -070061import java.util.concurrent.ConcurrentHashMap;
Simon Huntd5b96732016-07-08 13:22:27 -070062
63import static com.google.common.base.Preconditions.checkNotNull;
Simon Hunt6a8cb4f2016-08-09 15:08:57 -070064import static org.onosproject.net.AnnotationKeys.LATITUDE;
65import static org.onosproject.net.AnnotationKeys.LONGITUDE;
Simon Hunt977aa052016-07-20 17:08:29 -070066import static org.onosproject.ui.model.topo.UiNode.LAYER_DEFAULT;
Simon Huntd5b96732016-07-08 13:22:27 -070067
68/**
69 * Facility for creating JSON messages to send to the topology view in the
70 * Web client.
71 */
72class Topo2Jsonifier {
73
Simon Hunt977aa052016-07-20 17:08:29 -070074 private static final String E_DEF_NOT_LAST =
75 "UiNode.LAYER_DEFAULT not last in layer list";
76 private static final String E_UNKNOWN_UI_NODE =
77 "Unknown subclass of UiNode: ";
78
Simon Hunt98189192016-07-29 19:02:27 -070079 private static final String REGION = "region";
80 private static final String DEVICE = "device";
81 private static final String HOST = "host";
82
83 private final Logger log = LoggerFactory.getLogger(getClass());
84
Simon Huntd5b96732016-07-08 13:22:27 -070085 private final ObjectMapper mapper = new ObjectMapper();
86
87 private ServiceDirectory directory;
88 private ClusterService clusterService;
89 private DeviceService deviceService;
90 private LinkService linkService;
91 private HostService hostService;
92 private MastershipService mastershipService;
93 private IntentService intentService;
94 private FlowRuleService flowService;
95 private StatisticService flowStatsService;
96 private PortStatisticsService portStatsService;
97 private TopologyService topologyService;
98 private TunnelService tunnelService;
99
100
Simon Hunt6a8cb4f2016-08-09 15:08:57 -0700101 // NOTE: we'll stick this here for now, but maybe there is a better home?
102 // (this is not distributed across the cluster)
103 private static Map<String, ObjectNode> metaUi = new ConcurrentHashMap<>();
104
105
Simon Huntd5b96732016-07-08 13:22:27 -0700106 /**
107 * Creates an instance with a reference to the services directory, so that
108 * additional information about network elements may be looked up on
109 * on the fly.
110 *
111 * @param directory service directory
112 */
113 Topo2Jsonifier(ServiceDirectory directory) {
114 this.directory = checkNotNull(directory, "Directory cannot be null");
115
116 clusterService = directory.get(ClusterService.class);
117 deviceService = directory.get(DeviceService.class);
118 linkService = directory.get(LinkService.class);
119 hostService = directory.get(HostService.class);
120 mastershipService = directory.get(MastershipService.class);
121 intentService = directory.get(IntentService.class);
122 flowService = directory.get(FlowRuleService.class);
123 flowStatsService = directory.get(StatisticService.class);
124 portStatsService = directory.get(PortStatisticsService.class);
125 topologyService = directory.get(TopologyService.class);
126 tunnelService = directory.get(TunnelService.class);
Simon Hunt977aa052016-07-20 17:08:29 -0700127 }
Simon Huntd5b96732016-07-08 13:22:27 -0700128
Simon Hunt977aa052016-07-20 17:08:29 -0700129 // for unit testing
130 Topo2Jsonifier() {
Simon Huntd5b96732016-07-08 13:22:27 -0700131 }
132
133 private ObjectNode objectNode() {
134 return mapper.createObjectNode();
135 }
136
137 private ArrayNode arrayNode() {
138 return mapper.createArrayNode();
139 }
140
141 private String nullIsEmpty(Object o) {
142 return o == null ? "" : o.toString();
143 }
144
145
146 /**
147 * Returns a JSON representation of the cluster members (ONOS instances).
148 *
149 * @param instances the instance model objects
150 * @return a JSON representation of the data
151 */
152 ObjectNode instances(List<UiClusterMember> instances) {
153 NodeId local = clusterService.getLocalNode().id();
154 ObjectNode payload = objectNode();
155
156 ArrayNode members = arrayNode();
157 payload.set("members", members);
158 for (UiClusterMember member : instances) {
159 members.add(json(member, member.id().equals(local)));
160 }
161
162 return payload;
163 }
164
165 private ObjectNode json(UiClusterMember member, boolean isUiAttached) {
Steven Burrowsad75aa22016-12-14 17:17:24 -0500166 int switchCount = mastershipService.getDevicesOf(member.id()).size();
Simon Huntd5b96732016-07-08 13:22:27 -0700167 return objectNode()
168 .put("id", member.id().toString())
169 .put("ip", member.ip().toString())
170 .put("online", member.isOnline())
171 .put("ready", member.isReady())
172 .put("uiAttached", isUiAttached)
Steven Burrowsad75aa22016-12-14 17:17:24 -0500173 .put("switches", switchCount);
Simon Huntd5b96732016-07-08 13:22:27 -0700174 }
175
176 /**
177 * Returns a JSON representation of the layout to use for displaying in
Simon Huntf836a872016-08-10 17:37:36 -0700178 * the topology view. The identifiers and names of regions from the
179 * current to the root is included, so that the bread-crumb widget can
180 * be rendered.
Simon Huntd5b96732016-07-08 13:22:27 -0700181 *
182 * @param layout the layout to transform
Simon Huntf836a872016-08-10 17:37:36 -0700183 * @param crumbs list of layouts in bread-crumb order
Simon Huntd5b96732016-07-08 13:22:27 -0700184 * @return a JSON representation of the data
185 */
Simon Huntf836a872016-08-10 17:37:36 -0700186 ObjectNode layout(UiTopoLayout layout, List<UiTopoLayout> crumbs) {
187 ObjectNode result = objectNode()
Simon Huntd5b96732016-07-08 13:22:27 -0700188 .put("id", layout.id().toString())
189 .put("parent", nullIsEmpty(layout.parent()))
190 .put("region", nullIsEmpty(layout.regionId()))
Simon Huntf836a872016-08-10 17:37:36 -0700191 .put("regionName", UiRegion.safeName(layout.region()));
192 addCrumbs(result, crumbs);
193 return result;
Simon Huntd5b96732016-07-08 13:22:27 -0700194 }
195
Simon Huntf836a872016-08-10 17:37:36 -0700196 private void addCrumbs(ObjectNode result, List<UiTopoLayout> crumbs) {
197 ArrayNode trail = arrayNode();
198 crumbs.forEach(c -> {
199 ObjectNode n = objectNode()
200 .put("id", c.regionId().toString())
201 .put("name", UiRegion.safeName(c.region()));
202 trail.add(n);
203 });
204 result.set("crumbs", trail);
Simon Huntd5b96732016-07-08 13:22:27 -0700205 }
206
207 /**
208 * Returns a JSON representation of the region to display in the topology
209 * view.
210 *
Simon Hunt977aa052016-07-20 17:08:29 -0700211 * @param region the region to transform to JSON
212 * @param subRegions the subregions within this region
Simon Huntc13082f2016-08-03 21:20:23 -0700213 * @param links the links within this region
Simon Huntd5b96732016-07-08 13:22:27 -0700214 * @return a JSON representation of the data
215 */
Simon Huntc13082f2016-08-03 21:20:23 -0700216 ObjectNode region(UiRegion region, Set<UiRegion> subRegions,
217 List<UiSynthLink> links) {
Simon Huntd5b96732016-07-08 13:22:27 -0700218 ObjectNode payload = objectNode();
Simon Huntd5b96732016-07-08 13:22:27 -0700219 if (region == null) {
220 payload.put("note", "no-region");
221 return payload;
222 }
Simon Hunt977aa052016-07-20 17:08:29 -0700223 payload.put("id", region.idAsString());
Simon Huntcd508a62016-10-27 12:47:24 -0700224 payload.set("subregions", jsonSubRegions(subRegions));
225 payload.set("links", jsonLinks(links));
Simon Huntc13082f2016-08-03 21:20:23 -0700226
Simon Hunt977aa052016-07-20 17:08:29 -0700227 List<String> layerTags = region.layerOrder();
228 List<Set<UiNode>> splitDevices = splitByLayer(layerTags, region.devices());
229 List<Set<UiNode>> splitHosts = splitByLayer(layerTags, region.hosts());
Simon Huntd5b96732016-07-08 13:22:27 -0700230
Simon Hunt977aa052016-07-20 17:08:29 -0700231 payload.set("devices", jsonGrouped(splitDevices));
232 payload.set("hosts", jsonGrouped(splitHosts));
Simon Hunt977aa052016-07-20 17:08:29 -0700233 payload.set("layerOrder", jsonStrings(layerTags));
Simon Huntd5b96732016-07-08 13:22:27 -0700234
235 return payload;
236 }
237
Simon Hunt977aa052016-07-20 17:08:29 -0700238 private ArrayNode jsonSubRegions(Set<UiRegion> subregions) {
239 ArrayNode kids = arrayNode();
Simon Huntc13082f2016-08-03 21:20:23 -0700240 subregions.forEach(s -> kids.add(jsonClosedRegion(s)));
Simon Hunt977aa052016-07-20 17:08:29 -0700241 return kids;
242 }
243
Simon Huntc13082f2016-08-03 21:20:23 -0700244 private JsonNode jsonLinks(List<UiSynthLink> links) {
245 ArrayNode synthLinks = arrayNode();
246 links.forEach(l -> synthLinks.add(json(l)));
247 return synthLinks;
248 }
249
Simon Hunt977aa052016-07-20 17:08:29 -0700250 private ArrayNode jsonStrings(List<String> strings) {
251 ArrayNode array = arrayNode();
252 strings.forEach(array::add);
253 return array;
254 }
255
Simon Hunt977aa052016-07-20 17:08:29 -0700256 private ArrayNode jsonGrouped(List<Set<UiNode>> groupedNodes) {
257 ArrayNode result = arrayNode();
258 groupedNodes.forEach(g -> {
259 ArrayNode subset = arrayNode();
260 g.forEach(n -> subset.add(json(n)));
261 result.add(subset);
262 });
263 return result;
264 }
265
Steven Burrowsad75aa22016-12-14 17:17:24 -0500266 // Returns the name of the master node for the specified device id.
267 private String master(DeviceId deviceId) {
268 NodeId master = mastershipService.getMasterFor(deviceId);
269 return master != null ? master.toString() : "";
270 }
Simon Hunt977aa052016-07-20 17:08:29 -0700271
272 private ObjectNode json(UiNode node) {
273 if (node instanceof UiRegion) {
274 return jsonClosedRegion((UiRegion) node);
275 }
276 if (node instanceof UiDevice) {
277 return json((UiDevice) node);
278 }
279 if (node instanceof UiHost) {
280 return json((UiHost) node);
281 }
282 throw new IllegalStateException(E_UNKNOWN_UI_NODE + node.getClass());
283 }
284
Simon Huntd5b96732016-07-08 13:22:27 -0700285 private ObjectNode json(UiDevice device) {
286 ObjectNode node = objectNode()
Simon Hunt977aa052016-07-20 17:08:29 -0700287 .put("id", device.idAsString())
Simon Hunt98189192016-07-29 19:02:27 -0700288 .put("nodeType", DEVICE)
Simon Huntd5b96732016-07-08 13:22:27 -0700289 .put("type", device.type())
Simon Hunt3d712522016-08-11 11:20:44 -0700290 .put("online", deviceService.isAvailable(device.id()))
Steven Burrowsad75aa22016-12-14 17:17:24 -0500291 .put("master", master(device.id()))
Simon Huntd5b96732016-07-08 13:22:27 -0700292 .put("layer", device.layer());
293
Simon Hunt6a8cb4f2016-08-09 15:08:57 -0700294 Device d = device.backingDevice();
295
296 addProps(node, d);
297 addGeoLocation(node, d);
298 addMetaUi(node, device.idAsString());
Simon Huntd5b96732016-07-08 13:22:27 -0700299
300 return node;
301 }
302
Simon Hunt53612212016-12-04 17:19:52 -0800303 private void addProps(ObjectNode node, Annotated a) {
304 Annotations annot = a.annotations();
Simon Hunt6a8cb4f2016-08-09 15:08:57 -0700305 ObjectNode props = objectNode();
306 if (annot != null) {
307 annot.keys().forEach(k -> props.put(k, annot.value(k)));
308 }
309 node.set("props", props);
310 }
Simon Huntd5b96732016-07-08 13:22:27 -0700311
Simon Hunt6a8cb4f2016-08-09 15:08:57 -0700312 private void addMetaUi(ObjectNode node, String metaInstanceId) {
313 ObjectNode meta = metaUi.get(metaInstanceId);
314 if (meta != null) {
315 node.set("metaUi", meta);
316 }
317 }
318
319 private void addGeoLocation(ObjectNode node, Annotated a) {
320 List<String> lngLat = getAnnotValues(a, LONGITUDE, LATITUDE);
321 if (lngLat != null) {
322 try {
323 double lng = Double.parseDouble(lngLat.get(0));
324 double lat = Double.parseDouble(lngLat.get(1));
325 ObjectNode loc = objectNode()
326 .put("type", "lnglat")
327 .put("lng", lng)
328 .put("lat", lat);
329 node.set("location", loc);
330
331 } catch (NumberFormatException e) {
332 log.warn("Invalid geo data: longitude={}, latitude={}",
333 lngLat.get(0), lngLat.get(1));
334 }
335 } else {
336 log.debug("No geo lng/lat for {}", a);
337 }
338 }
339
Steven Burrows583f4be2016-11-04 14:06:50 +0100340 private void addIps(ObjectNode node, Host h) {
341 Set<IpAddress> ips = h.ipAddresses();
342
343 ArrayNode a = arrayNode();
344 for (IpAddress ip : ips) {
345 a.add(ip.toString());
346 }
347
348 node.set("ips", a);
349 }
350
Simon Hunt6a8cb4f2016-08-09 15:08:57 -0700351 // return list of string values from annotated instance, for given keys
352 // return null if any keys are not present
353 List<String> getAnnotValues(Annotated a, String... annotKeys) {
354 List<String> result = new ArrayList<>(annotKeys.length);
355 for (String k : annotKeys) {
356 String v = a.annotations().value(k);
357 if (v == null) {
358 return null;
359 }
360 result.add(v);
361 }
362 return result;
363 }
364
365 // derive JSON object from annotations
366 private ObjectNode props(Annotations annotations) {
367 ObjectNode p = objectNode();
368 if (annotations != null) {
369 annotations.keys().forEach(k -> p.put(k, annotations.value(k)));
370 }
371 return p;
Simon Huntd5b96732016-07-08 13:22:27 -0700372 }
373
374 private ObjectNode json(UiHost host) {
Steven Burrows583f4be2016-11-04 14:06:50 +0100375 ObjectNode node = objectNode()
Simon Hunt977aa052016-07-20 17:08:29 -0700376 .put("id", host.idAsString())
Simon Hunt98189192016-07-29 19:02:27 -0700377 .put("nodeType", HOST)
Simon Huntd5b96732016-07-08 13:22:27 -0700378 .put("layer", host.layer());
379 // TODO: complete host details
Steven Burrows583f4be2016-11-04 14:06:50 +0100380 Host h = host.backingHost();
381
382 addIps(node, h);
383 addGeoLocation(node, h);
384 addMetaUi(node, host.idAsString());
385
386 return node;
Simon Huntd5b96732016-07-08 13:22:27 -0700387 }
388
Simon Huntc13082f2016-08-03 21:20:23 -0700389 private ObjectNode json(UiSynthLink sLink) {
390 UiLink uLink = sLink.link();
Simon Hunt3d712522016-08-11 11:20:44 -0700391 ObjectNode data = objectNode()
Simon Huntc13082f2016-08-03 21:20:23 -0700392 .put("id", uLink.idAsString())
393 .put("epA", uLink.endPointA())
394 .put("epB", uLink.endPointB())
395 .put("type", uLink.type());
Simon Hunt3d712522016-08-11 11:20:44 -0700396 String pA = uLink.endPortA();
397 String pB = uLink.endPortB();
398 if (pA != null) {
399 data.put("portA", pA);
400 }
401 if (pB != null) {
402 data.put("portB", pB);
403 }
404 return data;
Simon Huntd5b96732016-07-08 13:22:27 -0700405 }
406
407
Simon Hunt977aa052016-07-20 17:08:29 -0700408 private ObjectNode jsonClosedRegion(UiRegion region) {
Steven Burrows482d9502016-09-27 11:24:58 -0700409 ObjectNode node = objectNode()
Simon Huntb1ce2602016-07-23 14:04:31 -0700410 .put("id", region.idAsString())
Simon Huntf836a872016-08-10 17:37:36 -0700411 .put("name", region.name())
Simon Hunt98189192016-07-29 19:02:27 -0700412 .put("nodeType", REGION)
Steven Burrows19e6e4f2016-10-05 13:27:07 -0500413 .put("nDevs", region.deviceCount())
414 .put("nHosts", region.hostCount());
Simon Hunt53612212016-12-04 17:19:52 -0800415
416 Region r = region.backingRegion();
417 addGeoLocation(node, r);
418 addProps(node, r);
Steven Burrows482d9502016-09-27 11:24:58 -0700419
420 addMetaUi(node, region.idAsString());
421 return node;
Simon Hunt977aa052016-07-20 17:08:29 -0700422 }
423
Simon Hunt98189192016-07-29 19:02:27 -0700424 /**
425 * Returns a JSON array representation of a set of regions/devices. Note
426 * that the information is sufficient for showing regions as nodes.
427 *
428 * @param nodes the nodes
429 * @return a JSON representation of the nodes
430 */
431 public ArrayNode closedNodes(Set<UiNode> nodes) {
432 ArrayNode array = arrayNode();
Simon Huntc13082f2016-08-03 21:20:23 -0700433 for (UiNode node : nodes) {
Simon Hunt98189192016-07-29 19:02:27 -0700434 if (node instanceof UiRegion) {
435 array.add(jsonClosedRegion((UiRegion) node));
436 } else if (node instanceof UiDevice) {
437 array.add(json((UiDevice) node));
438 } else {
439 log.warn("Unexpected node instance: {}", node.getClass());
440 }
441 }
442 return array;
443 }
Simon Hunt977aa052016-07-20 17:08:29 -0700444
445 /**
446 * Returns a JSON array representation of a list of regions. Note that the
447 * information about each region is limited to what needs to be used to
448 * show the regions as nodes on the view.
449 *
450 * @param regions the regions
451 * @return a JSON representation of the minimal region information
452 */
453 public ArrayNode closedRegions(Set<UiRegion> regions) {
454 ArrayNode array = arrayNode();
455 for (UiRegion r : regions) {
456 array.add(jsonClosedRegion(r));
457 }
458 return array;
459 }
460
461 /**
462 * Returns a JSON array representation of a list of devices.
463 *
464 * @param devices the devices
465 * @return a JSON representation of the devices
466 */
467 public ArrayNode devices(Set<UiDevice> devices) {
468 ArrayNode array = arrayNode();
469 for (UiDevice device : devices) {
470 array.add(json(device));
471 }
472 return array;
473 }
474
475 /**
476 * Returns a JSON array representation of a list of hosts.
477 *
478 * @param hosts the hosts
479 * @return a JSON representation of the hosts
480 */
481 public ArrayNode hosts(Set<UiHost> hosts) {
482 ArrayNode array = arrayNode();
483 for (UiHost host : hosts) {
484 array.add(json(host));
485 }
486 return array;
487 }
488
Simon Hunt977aa052016-07-20 17:08:29 -0700489 // package-private for unit testing
490 List<Set<UiNode>> splitByLayer(List<String> layerTags,
491 Set<? extends UiNode> nodes) {
492 final int nLayers = layerTags.size();
493 if (!layerTags.get(nLayers - 1).equals(LAYER_DEFAULT)) {
494 throw new IllegalArgumentException(E_DEF_NOT_LAST);
495 }
496
497 List<Set<UiNode>> splitList = new ArrayList<>(layerTags.size());
498 Map<String, Set<UiNode>> byLayer = new HashMap<>(layerTags.size());
499
500 for (String tag : layerTags) {
501 Set<UiNode> set = new HashSet<>();
502 byLayer.put(tag, set);
503 splitList.add(set);
504 }
505
506 for (UiNode n : nodes) {
507 String which = n.layer();
508 if (!layerTags.contains(which)) {
509 which = LAYER_DEFAULT;
510 }
511 byLayer.get(which).add(n);
512 }
513
514 return splitList;
515 }
Steven Burrowse7cc3082016-09-27 11:24:58 -0700516
517 /**
518 * Stores the memento for an element.
519 * This method assumes the payload has an id String, memento ObjectNode
520 *
521 * @param payload event payload
522 */
523 void updateMeta(ObjectNode payload) {
524
525 String id = JsonUtils.string(payload, "id");
526 metaUi.put(id, JsonUtils.node(payload, "memento"));
527
528 log.debug("Storing metadata for {}", id);
529 }
Simon Huntd5b96732016-07-08 13:22:27 -0700530}