blob: b407171ec08e91d1ed36d88d404f0b4518619a4a [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;
Steven Burrows86b74fc2017-02-22 00:15:16 +000044import org.onosproject.ui.UiExtensionService;
Simon Hunt95f4b422017-03-03 13:49:05 -080045import org.onosproject.ui.UiPreferencesService;
Steven Burrows86b74fc2017-02-22 00:15:16 +000046import org.onosproject.ui.UiTopoMap;
47import org.onosproject.ui.UiTopoMapFactory;
Steven Burrows512b6272016-12-19 14:09:45 -050048import org.onosproject.ui.impl.topo.model.UiModelEvent;
Simon Huntd5b96732016-07-08 13:22:27 -070049import org.onosproject.ui.model.topo.UiClusterMember;
50import org.onosproject.ui.model.topo.UiDevice;
Simon Hunt8eac4ae2017-01-20 12:56:45 -080051import org.onosproject.ui.model.topo.UiElement;
Simon Huntd5b96732016-07-08 13:22:27 -070052import org.onosproject.ui.model.topo.UiHost;
53import org.onosproject.ui.model.topo.UiLink;
Simon Hunt977aa052016-07-20 17:08:29 -070054import org.onosproject.ui.model.topo.UiNode;
Simon Huntd5b96732016-07-08 13:22:27 -070055import org.onosproject.ui.model.topo.UiRegion;
Simon Huntc13082f2016-08-03 21:20:23 -070056import org.onosproject.ui.model.topo.UiSynthLink;
Simon Huntd5b96732016-07-08 13:22:27 -070057import org.onosproject.ui.model.topo.UiTopoLayout;
Simon Hunt98189192016-07-29 19:02:27 -070058import org.slf4j.Logger;
59import org.slf4j.LoggerFactory;
Simon Huntd5b96732016-07-08 13:22:27 -070060
Simon Hunt977aa052016-07-20 17:08:29 -070061import java.util.ArrayList;
62import java.util.HashMap;
63import java.util.HashSet;
Simon Huntd5b96732016-07-08 13:22:27 -070064import java.util.List;
Simon Hunt977aa052016-07-20 17:08:29 -070065import java.util.Map;
66import java.util.Set;
Simon Hunt6a8cb4f2016-08-09 15:08:57 -070067import java.util.concurrent.ConcurrentHashMap;
Simon Huntd5b96732016-07-08 13:22:27 -070068
69import static com.google.common.base.Preconditions.checkNotNull;
Simon Huntbc30e682017-02-15 18:39:23 -080070import static org.onosproject.net.AnnotationKeys.GRID_X;
71import static org.onosproject.net.AnnotationKeys.GRID_Y;
Simon Hunt6a8cb4f2016-08-09 15:08:57 -070072import static org.onosproject.net.AnnotationKeys.LATITUDE;
73import static org.onosproject.net.AnnotationKeys.LONGITUDE;
Simon Hunt977aa052016-07-20 17:08:29 -070074import static org.onosproject.ui.model.topo.UiNode.LAYER_DEFAULT;
Simon Huntd5b96732016-07-08 13:22:27 -070075
76/**
77 * Facility for creating JSON messages to send to the topology view in the
78 * Web client.
79 */
Simon Hunt537bc762016-12-20 12:15:13 -080080public class Topo2Jsonifier {
Simon Huntd5b96732016-07-08 13:22:27 -070081
Simon Hunt977aa052016-07-20 17:08:29 -070082 private static final String E_DEF_NOT_LAST =
83 "UiNode.LAYER_DEFAULT not last in layer list";
84 private static final String E_UNKNOWN_UI_NODE =
85 "Unknown subclass of UiNode: ";
86
Simon Hunt2521d5f2017-03-20 18:17:28 -070087 private static final String CONTEXT_KEY_DELIM = "_";
88 private static final String NO_CONTEXT = "";
Simon Huntf0c6f542017-03-22 18:31:18 -070089 private static final String ZOOM_KEY = "layoutZoom";
Simon Hunt2521d5f2017-03-20 18:17:28 -070090
Simon Hunt98189192016-07-29 19:02:27 -070091 private static final String REGION = "region";
92 private static final String DEVICE = "device";
93 private static final String HOST = "host";
Steven Burrows512b6272016-12-19 14:09:45 -050094 private static final String TYPE = "type";
95 private static final String SUBJECT = "subject";
Simon Hunt8eac4ae2017-01-20 12:56:45 -080096 private static final String DATA = "data";
97 private static final String MEMO = "memo";
Simon Hunt98189192016-07-29 19:02:27 -070098
Simon Hunt95f4b422017-03-03 13:49:05 -080099 private static final String GEO = "geo";
100 private static final String GRID = "grid";
Simon Hunt95f4b422017-03-03 13:49:05 -0800101
Simon Hunt98189192016-07-29 19:02:27 -0700102 private final Logger log = LoggerFactory.getLogger(getClass());
103
Simon Huntd5b96732016-07-08 13:22:27 -0700104 private final ObjectMapper mapper = new ObjectMapper();
105
Simon Hunt95f4b422017-03-03 13:49:05 -0800106 // preferences are stored per user name...
107 private final String userName;
108
Simon Huntd5b96732016-07-08 13:22:27 -0700109 private ServiceDirectory directory;
110 private ClusterService clusterService;
111 private DeviceService deviceService;
112 private LinkService linkService;
113 private HostService hostService;
114 private MastershipService mastershipService;
115 private IntentService intentService;
116 private FlowRuleService flowService;
117 private StatisticService flowStatsService;
118 private PortStatisticsService portStatsService;
119 private TopologyService topologyService;
120 private TunnelService tunnelService;
Steven Burrows86b74fc2017-02-22 00:15:16 +0000121 private UiExtensionService uiextService;
Simon Hunt95f4b422017-03-03 13:49:05 -0800122 private UiPreferencesService prefService;
Simon Huntd5b96732016-07-08 13:22:27 -0700123
124
Simon Hunt6a8cb4f2016-08-09 15:08:57 -0700125 // NOTE: we'll stick this here for now, but maybe there is a better home?
126 // (this is not distributed across the cluster)
127 private static Map<String, ObjectNode> metaUi = new ConcurrentHashMap<>();
128
129
Simon Huntd5b96732016-07-08 13:22:27 -0700130 /**
131 * Creates an instance with a reference to the services directory, so that
132 * additional information about network elements may be looked up on
133 * on the fly.
134 *
135 * @param directory service directory
Simon Hunt95f4b422017-03-03 13:49:05 -0800136 * @param userName logged in user name
Simon Huntd5b96732016-07-08 13:22:27 -0700137 */
Simon Hunt95f4b422017-03-03 13:49:05 -0800138 public Topo2Jsonifier(ServiceDirectory directory, String userName) {
Simon Huntd5b96732016-07-08 13:22:27 -0700139 this.directory = checkNotNull(directory, "Directory cannot be null");
Simon Hunt95f4b422017-03-03 13:49:05 -0800140 this.userName = checkNotNull(userName, "User name cannot be null");
Simon Huntd5b96732016-07-08 13:22:27 -0700141
142 clusterService = directory.get(ClusterService.class);
143 deviceService = directory.get(DeviceService.class);
144 linkService = directory.get(LinkService.class);
145 hostService = directory.get(HostService.class);
146 mastershipService = directory.get(MastershipService.class);
147 intentService = directory.get(IntentService.class);
148 flowService = directory.get(FlowRuleService.class);
149 flowStatsService = directory.get(StatisticService.class);
150 portStatsService = directory.get(PortStatisticsService.class);
151 topologyService = directory.get(TopologyService.class);
152 tunnelService = directory.get(TunnelService.class);
Steven Burrows86b74fc2017-02-22 00:15:16 +0000153 uiextService = directory.get(UiExtensionService.class);
Simon Hunt95f4b422017-03-03 13:49:05 -0800154 prefService = directory.get(UiPreferencesService.class);
Simon Hunt977aa052016-07-20 17:08:29 -0700155 }
Simon Huntd5b96732016-07-08 13:22:27 -0700156
Simon Hunt977aa052016-07-20 17:08:29 -0700157 // for unit testing
158 Topo2Jsonifier() {
Simon Hunt95f4b422017-03-03 13:49:05 -0800159 userName = "(unit-test)";
Simon Huntd5b96732016-07-08 13:22:27 -0700160 }
161
162 private ObjectNode objectNode() {
163 return mapper.createObjectNode();
164 }
165
166 private ArrayNode arrayNode() {
167 return mapper.createArrayNode();
168 }
169
170 private String nullIsEmpty(Object o) {
171 return o == null ? "" : o.toString();
172 }
173
174
175 /**
176 * Returns a JSON representation of the cluster members (ONOS instances).
177 *
178 * @param instances the instance model objects
179 * @return a JSON representation of the data
180 */
181 ObjectNode instances(List<UiClusterMember> instances) {
182 NodeId local = clusterService.getLocalNode().id();
183 ObjectNode payload = objectNode();
184
185 ArrayNode members = arrayNode();
186 payload.set("members", members);
187 for (UiClusterMember member : instances) {
188 members.add(json(member, member.id().equals(local)));
189 }
190
191 return payload;
192 }
193
194 private ObjectNode json(UiClusterMember member, boolean isUiAttached) {
Steven Burrowsad75aa22016-12-14 17:17:24 -0500195 int switchCount = mastershipService.getDevicesOf(member.id()).size();
Simon Huntd5b96732016-07-08 13:22:27 -0700196 return objectNode()
197 .put("id", member.id().toString())
198 .put("ip", member.ip().toString())
199 .put("online", member.isOnline())
200 .put("ready", member.isReady())
201 .put("uiAttached", isUiAttached)
Steven Burrowsad75aa22016-12-14 17:17:24 -0500202 .put("switches", switchCount);
Simon Huntd5b96732016-07-08 13:22:27 -0700203 }
204
205 /**
206 * Returns a JSON representation of the layout to use for displaying in
Simon Huntf836a872016-08-10 17:37:36 -0700207 * the topology view. The identifiers and names of regions from the
208 * current to the root is included, so that the bread-crumb widget can
209 * be rendered.
Simon Huntd5b96732016-07-08 13:22:27 -0700210 *
211 * @param layout the layout to transform
Simon Huntf836a872016-08-10 17:37:36 -0700212 * @param crumbs list of layouts in bread-crumb order
Simon Huntd5b96732016-07-08 13:22:27 -0700213 * @return a JSON representation of the data
214 */
Simon Huntf7e7d4a2017-03-24 15:22:20 -0700215 ObjectNode layout(UiTopoLayout layout, List<UiTopoLayout> crumbs) {
Simon Huntf836a872016-08-10 17:37:36 -0700216 ObjectNode result = objectNode()
Simon Huntd5b96732016-07-08 13:22:27 -0700217 .put("id", layout.id().toString())
218 .put("parent", nullIsEmpty(layout.parent()))
219 .put("region", nullIsEmpty(layout.regionId()))
Simon Huntf836a872016-08-10 17:37:36 -0700220 .put("regionName", UiRegion.safeName(layout.region()));
221 addCrumbs(result, crumbs);
Simon Huntf7e7d4a2017-03-24 15:22:20 -0700222 addBgRef(result, layout);
Simon Huntf836a872016-08-10 17:37:36 -0700223 return result;
Simon Huntd5b96732016-07-08 13:22:27 -0700224 }
225
Simon Huntf7e7d4a2017-03-24 15:22:20 -0700226 private void addBgRef(ObjectNode result, UiTopoLayout layout) {
Steven Burrows86b74fc2017-02-22 00:15:16 +0000227 String mapId = layout.geomap();
228 String sprId = layout.sprites();
229
230 if (mapId != null) {
Simon Hunt95f4b422017-03-03 13:49:05 -0800231 result.put("bgType", GEO).put("bgId", mapId);
Steven Burrows86b74fc2017-02-22 00:15:16 +0000232 addMapParameters(result, mapId);
233 } else if (sprId != null) {
Simon Hunt95f4b422017-03-03 13:49:05 -0800234 result.put("bgType", GRID).put("bgId", sprId);
Steven Burrows86b74fc2017-02-22 00:15:16 +0000235 }
Simon Huntf0c6f542017-03-22 18:31:18 -0700236
Simon Huntf7e7d4a2017-03-24 15:22:20 -0700237 attachZoomData(result, layout);
Simon Hunt95f4b422017-03-03 13:49:05 -0800238 }
239
Simon Huntf7e7d4a2017-03-24 15:22:20 -0700240 private void attachZoomData(ObjectNode result, UiTopoLayout layout) {
Steven Burrowsb43c1a92017-03-07 17:13:28 +0000241
Simon Huntf0c6f542017-03-22 18:31:18 -0700242 ObjectNode zoomData = objectNode();
Steven Burrowsb43c1a92017-03-07 17:13:28 +0000243
Simon Huntf0c6f542017-03-22 18:31:18 -0700244 // first, set configured scale and offset
245 addCfgZoomData(zoomData, layout);
Simon Hunt95f4b422017-03-03 13:49:05 -0800246
Simon Huntf0c6f542017-03-22 18:31:18 -0700247 // next, retrieve user-set zoom data, if we have it
Simon Huntf7e7d4a2017-03-24 15:22:20 -0700248 String rid = layout.regionId().toString();
Simon Huntf0c6f542017-03-22 18:31:18 -0700249 ObjectNode userZoom = metaUi.get(contextKey(rid, ZOOM_KEY));
250 if (userZoom != null) {
251 zoomData.set("usr", userZoom);
Simon Hunt95f4b422017-03-03 13:49:05 -0800252 }
Simon Huntf0c6f542017-03-22 18:31:18 -0700253 result.set("bgZoom", zoomData);
254 }
Steven Burrowsb43c1a92017-03-07 17:13:28 +0000255
Simon Huntf0c6f542017-03-22 18:31:18 -0700256 private void addCfgZoomData(ObjectNode data, UiTopoLayout layout) {
257 ObjectNode zoom = objectNode();
258 zoom.put("scale", layout.scale());
259 zoom.put("offsetX", layout.offsetX());
260 zoom.put("offsetY", layout.offsetY());
261 data.set("cfg", zoom);
Steven Burrows86b74fc2017-02-22 00:15:16 +0000262 }
263
264 private void addMapParameters(ObjectNode result, String mapId) {
265
266 // TODO: This ought to be written more efficiently.
267
268 // ALSO: Should retrieving a UiTopoMap by ID be something that
269 // the UiExtensionService provides, along with other
270 // useful lookups?
271 //
272 // Or should it remain very basic / general?
273 //
274 // return uiextService.getTopoMap(String mapId);
275
276 final UiTopoMap[] map = {null};
277
278 uiextService.getExtensions().forEach(ext -> {
279 UiTopoMapFactory factory = ext.topoMapFactory();
280
281 // TODO: use .stream().filter(...) here
282 if (map[0] == null && factory != null) {
283 List<UiTopoMap> topoMaps = factory.geoMaps();
284
285 topoMaps.forEach(m -> {
286 if (map[0] == null && m.id().equals(mapId)) {
287 map[0] = m;
288 }
289 });
290 }
291 });
292
293 UiTopoMap m = map[0];
294 if (m != null) {
295 result.put("bgDesc", m.description())
296 .put("bgFilePath", m.filePath())
297 .put("bgDefaultScale", m.scale());
298 } else {
299 result.put("bgWarn", "no map registered with id: " + mapId);
Simon Huntbc30e682017-02-15 18:39:23 -0800300 }
301 }
302
Simon Huntf836a872016-08-10 17:37:36 -0700303 private void addCrumbs(ObjectNode result, List<UiTopoLayout> crumbs) {
304 ArrayNode trail = arrayNode();
305 crumbs.forEach(c -> {
306 ObjectNode n = objectNode()
307 .put("id", c.regionId().toString())
308 .put("name", UiRegion.safeName(c.region()));
309 trail.add(n);
310 });
311 result.set("crumbs", trail);
Simon Huntd5b96732016-07-08 13:22:27 -0700312 }
313
314 /**
315 * Returns a JSON representation of the region to display in the topology
316 * view.
317 *
Simon Hunt977aa052016-07-20 17:08:29 -0700318 * @param region the region to transform to JSON
319 * @param subRegions the subregions within this region
Simon Huntc13082f2016-08-03 21:20:23 -0700320 * @param links the links within this region
Simon Huntd5b96732016-07-08 13:22:27 -0700321 * @return a JSON representation of the data
322 */
Simon Huntc13082f2016-08-03 21:20:23 -0700323 ObjectNode region(UiRegion region, Set<UiRegion> subRegions,
324 List<UiSynthLink> links) {
Simon Huntd5b96732016-07-08 13:22:27 -0700325 ObjectNode payload = objectNode();
Simon Huntd5b96732016-07-08 13:22:27 -0700326 if (region == null) {
327 payload.put("note", "no-region");
328 return payload;
329 }
Simon Hunt2521d5f2017-03-20 18:17:28 -0700330
331 String ridStr = region.idAsString();
332
333 payload.put("id", ridStr);
334 payload.set("subregions", jsonSubRegions(ridStr, subRegions));
Simon Huntcd508a62016-10-27 12:47:24 -0700335 payload.set("links", jsonLinks(links));
Simon Huntc13082f2016-08-03 21:20:23 -0700336
Simon Hunt977aa052016-07-20 17:08:29 -0700337 List<String> layerTags = region.layerOrder();
338 List<Set<UiNode>> splitDevices = splitByLayer(layerTags, region.devices());
339 List<Set<UiNode>> splitHosts = splitByLayer(layerTags, region.hosts());
Simon Huntd5b96732016-07-08 13:22:27 -0700340
Simon Hunt2521d5f2017-03-20 18:17:28 -0700341 payload.set("devices", jsonGrouped(ridStr, splitDevices));
342 payload.set("hosts", jsonGrouped(ridStr, splitHosts));
Simon Hunt977aa052016-07-20 17:08:29 -0700343 payload.set("layerOrder", jsonStrings(layerTags));
Simon Huntd5b96732016-07-08 13:22:27 -0700344
345 return payload;
346 }
347
Simon Hunt2521d5f2017-03-20 18:17:28 -0700348 private ArrayNode jsonSubRegions(String ridStr, Set<UiRegion> subregions) {
Simon Hunt977aa052016-07-20 17:08:29 -0700349 ArrayNode kids = arrayNode();
Simon Hunt2521d5f2017-03-20 18:17:28 -0700350 subregions.forEach(s -> kids.add(jsonClosedRegion(ridStr, s)));
Simon Hunt977aa052016-07-20 17:08:29 -0700351 return kids;
352 }
353
Simon Huntc13082f2016-08-03 21:20:23 -0700354 private JsonNode jsonLinks(List<UiSynthLink> links) {
355 ArrayNode synthLinks = arrayNode();
356 links.forEach(l -> synthLinks.add(json(l)));
357 return synthLinks;
358 }
359
Simon Hunt977aa052016-07-20 17:08:29 -0700360 private ArrayNode jsonStrings(List<String> strings) {
361 ArrayNode array = arrayNode();
362 strings.forEach(array::add);
363 return array;
364 }
365
Simon Hunt2521d5f2017-03-20 18:17:28 -0700366 private ArrayNode jsonGrouped(String ridStr, List<Set<UiNode>> groupedNodes) {
Simon Hunt977aa052016-07-20 17:08:29 -0700367 ArrayNode result = arrayNode();
368 groupedNodes.forEach(g -> {
369 ArrayNode subset = arrayNode();
Simon Hunt2521d5f2017-03-20 18:17:28 -0700370 g.forEach(n -> subset.add(json(ridStr, n)));
Simon Hunt977aa052016-07-20 17:08:29 -0700371 result.add(subset);
372 });
373 return result;
374 }
375
Simon Hunt537bc762016-12-20 12:15:13 -0800376 /**
Simon Hunt8eac4ae2017-01-20 12:56:45 -0800377 * Creates a JSON representation of a UI element.
378 *
379 * @param element the source element
380 * @return a JSON representation of that element
381 */
382 public ObjectNode jsonUiElement(UiElement element) {
383 if (element instanceof UiNode) {
Simon Hunt2521d5f2017-03-20 18:17:28 -0700384 return json(NO_CONTEXT, (UiNode) element);
Simon Hunt8eac4ae2017-01-20 12:56:45 -0800385 }
386 if (element instanceof UiLink) {
387 return json((UiLink) element);
388 }
389
390 // TODO: UiClusterMember
391
392 // Unrecognized UiElement class
393 return objectNode()
394 .put("warning", "unknown UiElement... cannot encode")
395 .put("javaclass", element.getClass().toString());
396 }
397
398 /**
Simon Hunt537bc762016-12-20 12:15:13 -0800399 * Creates a JSON representation of a UI model event.
400 *
401 * @param modelEvent the source model event
402 * @return a JSON representation of that event
403 */
404 public ObjectNode jsonEvent(UiModelEvent modelEvent) {
Steven Burrows512b6272016-12-19 14:09:45 -0500405 ObjectNode payload = objectNode();
406 payload.put(TYPE, enumToString(modelEvent.type()));
407 payload.put(SUBJECT, modelEvent.subject().idAsString());
Simon Hunt8eac4ae2017-01-20 12:56:45 -0800408 payload.set(DATA, modelEvent.data());
409 payload.put(MEMO, modelEvent.memo());
Steven Burrows512b6272016-12-19 14:09:45 -0500410 return payload;
411 }
412
413 // TODO: Investigate why we can't do this inline
414 private String enumToString(Enum<?> e) {
415 return e.toString();
416 }
417
Steven Burrowsad75aa22016-12-14 17:17:24 -0500418 // Returns the name of the master node for the specified device id.
419 private String master(DeviceId deviceId) {
420 NodeId master = mastershipService.getMasterFor(deviceId);
421 return master != null ? master.toString() : "";
422 }
Simon Hunt977aa052016-07-20 17:08:29 -0700423
Simon Hunt2521d5f2017-03-20 18:17:28 -0700424 private ObjectNode json(String ridStr, UiNode node) {
Simon Hunt977aa052016-07-20 17:08:29 -0700425 if (node instanceof UiRegion) {
Simon Hunt2521d5f2017-03-20 18:17:28 -0700426 return jsonClosedRegion(ridStr, (UiRegion) node);
Simon Hunt977aa052016-07-20 17:08:29 -0700427 }
428 if (node instanceof UiDevice) {
Simon Hunt2521d5f2017-03-20 18:17:28 -0700429 return json(ridStr, (UiDevice) node);
Simon Hunt977aa052016-07-20 17:08:29 -0700430 }
431 if (node instanceof UiHost) {
Simon Hunt2521d5f2017-03-20 18:17:28 -0700432 return json(ridStr, (UiHost) node);
Simon Hunt977aa052016-07-20 17:08:29 -0700433 }
434 throw new IllegalStateException(E_UNKNOWN_UI_NODE + node.getClass());
435 }
436
Simon Hunt2521d5f2017-03-20 18:17:28 -0700437 private ObjectNode json(String ridStr, UiDevice device) {
Simon Huntd5b96732016-07-08 13:22:27 -0700438 ObjectNode node = objectNode()
Simon Hunt977aa052016-07-20 17:08:29 -0700439 .put("id", device.idAsString())
Simon Hunt98189192016-07-29 19:02:27 -0700440 .put("nodeType", DEVICE)
Simon Huntd5b96732016-07-08 13:22:27 -0700441 .put("type", device.type())
Simon Hunt3d712522016-08-11 11:20:44 -0700442 .put("online", deviceService.isAvailable(device.id()))
Steven Burrowsad75aa22016-12-14 17:17:24 -0500443 .put("master", master(device.id()))
Simon Huntd5b96732016-07-08 13:22:27 -0700444 .put("layer", device.layer());
445
Simon Hunt6a8cb4f2016-08-09 15:08:57 -0700446 Device d = device.backingDevice();
447
448 addProps(node, d);
Simon Huntbc30e682017-02-15 18:39:23 -0800449 addGeoGridLocation(node, d);
Simon Hunt2521d5f2017-03-20 18:17:28 -0700450 addMetaUi(node, ridStr, device.idAsString());
Simon Huntd5b96732016-07-08 13:22:27 -0700451
452 return node;
453 }
454
Simon Hunt53612212016-12-04 17:19:52 -0800455 private void addProps(ObjectNode node, Annotated a) {
456 Annotations annot = a.annotations();
Simon Hunt6a8cb4f2016-08-09 15:08:57 -0700457 ObjectNode props = objectNode();
458 if (annot != null) {
459 annot.keys().forEach(k -> props.put(k, annot.value(k)));
460 }
461 node.set("props", props);
462 }
Simon Huntd5b96732016-07-08 13:22:27 -0700463
Simon Hunt2521d5f2017-03-20 18:17:28 -0700464 private void addMetaUi(ObjectNode node, String ridStr, String metaInstanceId) {
465 String key = contextKey(ridStr, metaInstanceId);
466 ObjectNode meta = metaUi.get(key);
Simon Hunt6a8cb4f2016-08-09 15:08:57 -0700467 if (meta != null) {
468 node.set("metaUi", meta);
469 }
470 }
471
Simon Huntbc30e682017-02-15 18:39:23 -0800472 private void addGeoGridLocation(ObjectNode node, Annotated a) {
Simon Hunt6a8cb4f2016-08-09 15:08:57 -0700473 List<String> lngLat = getAnnotValues(a, LONGITUDE, LATITUDE);
Simon Huntbc30e682017-02-15 18:39:23 -0800474 List<String> gridYX = getAnnotValues(a, GRID_Y, GRID_X);
Simon Hunt6a8cb4f2016-08-09 15:08:57 -0700475
Simon Huntbc30e682017-02-15 18:39:23 -0800476 if (lngLat != null) {
477 attachLocation(node, "geo", "lng", "lat", lngLat);
478 } else if (gridYX != null) {
479 attachLocation(node, "grid", "gridY", "gridX", gridYX);
480 }
481 }
482
483 private void attachLocation(ObjectNode node, String locType,
484 String keyA, String keyB, List<String> values) {
485 try {
486 double valA = Double.parseDouble(values.get(0));
487 double valB = Double.parseDouble(values.get(1));
488 ObjectNode loc = objectNode()
489 .put("type", locType)
490 .put(keyA, valA)
491 .put(keyB, valB);
492 node.set("location", loc);
493
494 } catch (NumberFormatException e) {
495 log.warn("Invalid {} data: long/Y={}, lat/X={}",
496 locType, values.get(0), values.get(1));
Simon Hunt6a8cb4f2016-08-09 15:08:57 -0700497 }
498 }
499
Steven Burrows583f4be2016-11-04 14:06:50 +0100500 private void addIps(ObjectNode node, Host h) {
501 Set<IpAddress> ips = h.ipAddresses();
502
503 ArrayNode a = arrayNode();
504 for (IpAddress ip : ips) {
505 a.add(ip.toString());
506 }
507
508 node.set("ips", a);
509 }
510
Simon Hunt6a8cb4f2016-08-09 15:08:57 -0700511 // return list of string values from annotated instance, for given keys
512 // return null if any keys are not present
513 List<String> getAnnotValues(Annotated a, String... annotKeys) {
514 List<String> result = new ArrayList<>(annotKeys.length);
515 for (String k : annotKeys) {
516 String v = a.annotations().value(k);
517 if (v == null) {
518 return null;
519 }
520 result.add(v);
521 }
522 return result;
523 }
524
525 // derive JSON object from annotations
526 private ObjectNode props(Annotations annotations) {
527 ObjectNode p = objectNode();
528 if (annotations != null) {
529 annotations.keys().forEach(k -> p.put(k, annotations.value(k)));
530 }
531 return p;
Simon Huntd5b96732016-07-08 13:22:27 -0700532 }
533
Simon Hunt2521d5f2017-03-20 18:17:28 -0700534 private ObjectNode json(String ridStr, UiHost host) {
Steven Burrows583f4be2016-11-04 14:06:50 +0100535 ObjectNode node = objectNode()
Simon Hunt977aa052016-07-20 17:08:29 -0700536 .put("id", host.idAsString())
Simon Hunt98189192016-07-29 19:02:27 -0700537 .put("nodeType", HOST)
Simon Huntd5b96732016-07-08 13:22:27 -0700538 .put("layer", host.layer());
539 // TODO: complete host details
Steven Burrows583f4be2016-11-04 14:06:50 +0100540 Host h = host.backingHost();
541
542 addIps(node, h);
Simon Huntbc30e682017-02-15 18:39:23 -0800543 addProps(node, h);
544 addGeoGridLocation(node, h);
Simon Hunt2521d5f2017-03-20 18:17:28 -0700545 addMetaUi(node, ridStr, host.idAsString());
Steven Burrows583f4be2016-11-04 14:06:50 +0100546
547 return node;
Simon Huntd5b96732016-07-08 13:22:27 -0700548 }
549
Simon Huntc13082f2016-08-03 21:20:23 -0700550 private ObjectNode json(UiSynthLink sLink) {
Simon Hunt8eac4ae2017-01-20 12:56:45 -0800551 return json(sLink.link());
552 }
553
554 private ObjectNode json(UiLink link) {
Simon Hunt3d712522016-08-11 11:20:44 -0700555 ObjectNode data = objectNode()
Simon Hunt8eac4ae2017-01-20 12:56:45 -0800556 .put("id", link.idAsString())
557 .put("epA", link.endPointA())
558 .put("epB", link.endPointB())
559 .put("type", link.type());
560 String pA = link.endPortA();
561 String pB = link.endPortB();
Simon Hunt3d712522016-08-11 11:20:44 -0700562 if (pA != null) {
563 data.put("portA", pA);
564 }
565 if (pB != null) {
566 data.put("portB", pB);
567 }
568 return data;
Simon Huntd5b96732016-07-08 13:22:27 -0700569 }
570
571
Simon Hunt2521d5f2017-03-20 18:17:28 -0700572 private ObjectNode jsonClosedRegion(String ridStr, UiRegion region) {
Steven Burrows482d9502016-09-27 11:24:58 -0700573 ObjectNode node = objectNode()
Simon Huntb1ce2602016-07-23 14:04:31 -0700574 .put("id", region.idAsString())
Simon Huntf836a872016-08-10 17:37:36 -0700575 .put("name", region.name())
Simon Hunt98189192016-07-29 19:02:27 -0700576 .put("nodeType", REGION)
Steven Burrows19e6e4f2016-10-05 13:27:07 -0500577 .put("nDevs", region.deviceCount())
578 .put("nHosts", region.hostCount());
Simon Hunt2521d5f2017-03-20 18:17:28 -0700579 // TODO: device and host counts should take into account any nested
580 // subregions. i.e. should be the sum of all devices/hosts in
581 // all descendent subregions.
Simon Hunt53612212016-12-04 17:19:52 -0800582
583 Region r = region.backingRegion();
Simon Hunt2521d5f2017-03-20 18:17:28 -0700584 // this is location data, as injected via network configuration script
Simon Huntbc30e682017-02-15 18:39:23 -0800585 addGeoGridLocation(node, r);
Simon Hunt53612212016-12-04 17:19:52 -0800586 addProps(node, r);
Steven Burrows482d9502016-09-27 11:24:58 -0700587
Simon Hunt2521d5f2017-03-20 18:17:28 -0700588 // this may contain location data, as dragged by user
589 // (which should take precedence, over configured data)
590 addMetaUi(node, ridStr, region.idAsString());
Steven Burrows482d9502016-09-27 11:24:58 -0700591 return node;
Simon Hunt977aa052016-07-20 17:08:29 -0700592 }
593
Simon Hunt98189192016-07-29 19:02:27 -0700594 /**
595 * Returns a JSON array representation of a set of regions/devices. Note
596 * that the information is sufficient for showing regions as nodes.
Simon Hunt2521d5f2017-03-20 18:17:28 -0700597 * THe region ID string defines the context (which region) the node is
598 * being displayed in.
Simon Hunt98189192016-07-29 19:02:27 -0700599 *
Simon Hunt2521d5f2017-03-20 18:17:28 -0700600 * @param ridStr region-id string
Simon Huntf0c6f542017-03-22 18:31:18 -0700601 * @param nodes the nodes
Simon Hunt98189192016-07-29 19:02:27 -0700602 * @return a JSON representation of the nodes
603 */
Simon Hunt2521d5f2017-03-20 18:17:28 -0700604 public ArrayNode closedNodes(String ridStr, Set<UiNode> nodes) {
Simon Hunt98189192016-07-29 19:02:27 -0700605 ArrayNode array = arrayNode();
Simon Huntc13082f2016-08-03 21:20:23 -0700606 for (UiNode node : nodes) {
Simon Hunt98189192016-07-29 19:02:27 -0700607 if (node instanceof UiRegion) {
Simon Hunt2521d5f2017-03-20 18:17:28 -0700608 array.add(jsonClosedRegion(ridStr, (UiRegion) node));
Simon Hunt98189192016-07-29 19:02:27 -0700609 } else if (node instanceof UiDevice) {
Simon Hunt2521d5f2017-03-20 18:17:28 -0700610 array.add(json(ridStr, (UiDevice) node));
Simon Hunt98189192016-07-29 19:02:27 -0700611 } else {
612 log.warn("Unexpected node instance: {}", node.getClass());
613 }
614 }
615 return array;
616 }
Simon Hunt977aa052016-07-20 17:08:29 -0700617
Simon Hunt977aa052016-07-20 17:08:29 -0700618 // package-private for unit testing
619 List<Set<UiNode>> splitByLayer(List<String> layerTags,
620 Set<? extends UiNode> nodes) {
621 final int nLayers = layerTags.size();
622 if (!layerTags.get(nLayers - 1).equals(LAYER_DEFAULT)) {
623 throw new IllegalArgumentException(E_DEF_NOT_LAST);
624 }
625
626 List<Set<UiNode>> splitList = new ArrayList<>(layerTags.size());
627 Map<String, Set<UiNode>> byLayer = new HashMap<>(layerTags.size());
628
629 for (String tag : layerTags) {
630 Set<UiNode> set = new HashSet<>();
631 byLayer.put(tag, set);
632 splitList.add(set);
633 }
634
635 for (UiNode n : nodes) {
636 String which = n.layer();
637 if (!layerTags.contains(which)) {
638 which = LAYER_DEFAULT;
639 }
640 byLayer.get(which).add(n);
641 }
642
643 return splitList;
644 }
Steven Burrowse7cc3082016-09-27 11:24:58 -0700645
Simon Hunt2521d5f2017-03-20 18:17:28 -0700646
647 private String contextKey(String context, String key) {
648 return context + CONTEXT_KEY_DELIM + key;
649 }
650
Steven Burrowse7cc3082016-09-27 11:24:58 -0700651 /**
652 * Stores the memento for an element.
Simon Hunt2521d5f2017-03-20 18:17:28 -0700653 * This method assumes the payload has an id String, memento ObjectNode.
654 * The region-id string is used as a context within which to store the
655 * memento.
Steven Burrowse7cc3082016-09-27 11:24:58 -0700656 *
Simon Hunt2521d5f2017-03-20 18:17:28 -0700657 * @param ridStr region ID string
Steven Burrowse7cc3082016-09-27 11:24:58 -0700658 * @param payload event payload
659 */
Simon Hunt2521d5f2017-03-20 18:17:28 -0700660 void updateMeta(String ridStr, ObjectNode payload) {
Steven Burrowse7cc3082016-09-27 11:24:58 -0700661
662 String id = JsonUtils.string(payload, "id");
Simon Hunt2521d5f2017-03-20 18:17:28 -0700663 String key = contextKey(ridStr, id);
664 metaUi.put(key, JsonUtils.node(payload, "memento"));
Steven Burrowse7cc3082016-09-27 11:24:58 -0700665
Simon Hunt2521d5f2017-03-20 18:17:28 -0700666 log.debug("Storing metadata for {}", key);
Steven Burrowse7cc3082016-09-27 11:24:58 -0700667 }
Simon Huntd5b96732016-07-08 13:22:27 -0700668}