blob: a50401b8e1997e5ea8a419079c77509a0048e94e [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 Hunt95f4b422017-03-03 13:49:05 -080058import org.onosproject.ui.model.topo.UiTopoLayoutId;
Simon Hunt98189192016-07-29 19:02:27 -070059import org.slf4j.Logger;
60import org.slf4j.LoggerFactory;
Simon Huntd5b96732016-07-08 13:22:27 -070061
Simon Hunt977aa052016-07-20 17:08:29 -070062import java.util.ArrayList;
63import java.util.HashMap;
64import java.util.HashSet;
Simon Huntd5b96732016-07-08 13:22:27 -070065import java.util.List;
Simon Hunt977aa052016-07-20 17:08:29 -070066import java.util.Map;
67import java.util.Set;
Simon Hunt6a8cb4f2016-08-09 15:08:57 -070068import java.util.concurrent.ConcurrentHashMap;
Simon Huntd5b96732016-07-08 13:22:27 -070069
70import static com.google.common.base.Preconditions.checkNotNull;
Simon Huntbc30e682017-02-15 18:39:23 -080071import static org.onosproject.net.AnnotationKeys.GRID_X;
72import static org.onosproject.net.AnnotationKeys.GRID_Y;
Simon Hunt6a8cb4f2016-08-09 15:08:57 -070073import static org.onosproject.net.AnnotationKeys.LATITUDE;
74import static org.onosproject.net.AnnotationKeys.LONGITUDE;
Simon Hunt977aa052016-07-20 17:08:29 -070075import static org.onosproject.ui.model.topo.UiNode.LAYER_DEFAULT;
Simon Huntd5b96732016-07-08 13:22:27 -070076
77/**
78 * Facility for creating JSON messages to send to the topology view in the
79 * Web client.
80 */
Simon Hunt537bc762016-12-20 12:15:13 -080081public class Topo2Jsonifier {
Simon Huntd5b96732016-07-08 13:22:27 -070082
Simon Hunt977aa052016-07-20 17:08:29 -070083 private static final String E_DEF_NOT_LAST =
84 "UiNode.LAYER_DEFAULT not last in layer list";
85 private static final String E_UNKNOWN_UI_NODE =
86 "Unknown subclass of UiNode: ";
87
Simon Hunt2521d5f2017-03-20 18:17:28 -070088 private static final String CONTEXT_KEY_DELIM = "_";
89 private static final String NO_CONTEXT = "";
90
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";
101 private static final String PKEY_TOPO_ZOOM = "topo2_zoom";
102 private static final String ZOOM_SCALE = "zoomScale";
103 private static final String ZOOM_PAN_X = "zoomPanX";
104 private static final String ZOOM_PAN_Y = "zoomPanY";
105 private static final String DEFAULT_SCALE = "1.0";
106 private static final String DEFAULT_PAN = "0.0";
107
Simon Hunt98189192016-07-29 19:02:27 -0700108 private final Logger log = LoggerFactory.getLogger(getClass());
109
Simon Huntd5b96732016-07-08 13:22:27 -0700110 private final ObjectMapper mapper = new ObjectMapper();
111
Simon Hunt95f4b422017-03-03 13:49:05 -0800112 // preferences are stored per user name...
113 private final String userName;
114
Simon Huntd5b96732016-07-08 13:22:27 -0700115 private ServiceDirectory directory;
116 private ClusterService clusterService;
117 private DeviceService deviceService;
118 private LinkService linkService;
119 private HostService hostService;
120 private MastershipService mastershipService;
121 private IntentService intentService;
122 private FlowRuleService flowService;
123 private StatisticService flowStatsService;
124 private PortStatisticsService portStatsService;
125 private TopologyService topologyService;
126 private TunnelService tunnelService;
Steven Burrows86b74fc2017-02-22 00:15:16 +0000127 private UiExtensionService uiextService;
Simon Hunt95f4b422017-03-03 13:49:05 -0800128 private UiPreferencesService prefService;
Simon Huntd5b96732016-07-08 13:22:27 -0700129
130
Simon Hunt6a8cb4f2016-08-09 15:08:57 -0700131 // NOTE: we'll stick this here for now, but maybe there is a better home?
132 // (this is not distributed across the cluster)
133 private static Map<String, ObjectNode> metaUi = new ConcurrentHashMap<>();
134
135
Simon Huntd5b96732016-07-08 13:22:27 -0700136 /**
137 * Creates an instance with a reference to the services directory, so that
138 * additional information about network elements may be looked up on
139 * on the fly.
140 *
141 * @param directory service directory
Simon Hunt95f4b422017-03-03 13:49:05 -0800142 * @param userName logged in user name
Simon Huntd5b96732016-07-08 13:22:27 -0700143 */
Simon Hunt95f4b422017-03-03 13:49:05 -0800144 public Topo2Jsonifier(ServiceDirectory directory, String userName) {
Simon Huntd5b96732016-07-08 13:22:27 -0700145 this.directory = checkNotNull(directory, "Directory cannot be null");
Simon Hunt95f4b422017-03-03 13:49:05 -0800146 this.userName = checkNotNull(userName, "User name cannot be null");
Simon Huntd5b96732016-07-08 13:22:27 -0700147
148 clusterService = directory.get(ClusterService.class);
149 deviceService = directory.get(DeviceService.class);
150 linkService = directory.get(LinkService.class);
151 hostService = directory.get(HostService.class);
152 mastershipService = directory.get(MastershipService.class);
153 intentService = directory.get(IntentService.class);
154 flowService = directory.get(FlowRuleService.class);
155 flowStatsService = directory.get(StatisticService.class);
156 portStatsService = directory.get(PortStatisticsService.class);
157 topologyService = directory.get(TopologyService.class);
158 tunnelService = directory.get(TunnelService.class);
Steven Burrows86b74fc2017-02-22 00:15:16 +0000159 uiextService = directory.get(UiExtensionService.class);
Simon Hunt95f4b422017-03-03 13:49:05 -0800160 prefService = directory.get(UiPreferencesService.class);
Simon Hunt977aa052016-07-20 17:08:29 -0700161 }
Simon Huntd5b96732016-07-08 13:22:27 -0700162
Simon Hunt977aa052016-07-20 17:08:29 -0700163 // for unit testing
164 Topo2Jsonifier() {
Simon Hunt95f4b422017-03-03 13:49:05 -0800165 userName = "(unit-test)";
Simon Huntd5b96732016-07-08 13:22:27 -0700166 }
167
168 private ObjectNode objectNode() {
169 return mapper.createObjectNode();
170 }
171
172 private ArrayNode arrayNode() {
173 return mapper.createArrayNode();
174 }
175
176 private String nullIsEmpty(Object o) {
177 return o == null ? "" : o.toString();
178 }
179
180
181 /**
182 * Returns a JSON representation of the cluster members (ONOS instances).
183 *
184 * @param instances the instance model objects
185 * @return a JSON representation of the data
186 */
187 ObjectNode instances(List<UiClusterMember> instances) {
188 NodeId local = clusterService.getLocalNode().id();
189 ObjectNode payload = objectNode();
190
191 ArrayNode members = arrayNode();
192 payload.set("members", members);
193 for (UiClusterMember member : instances) {
194 members.add(json(member, member.id().equals(local)));
195 }
196
197 return payload;
198 }
199
200 private ObjectNode json(UiClusterMember member, boolean isUiAttached) {
Steven Burrowsad75aa22016-12-14 17:17:24 -0500201 int switchCount = mastershipService.getDevicesOf(member.id()).size();
Simon Huntd5b96732016-07-08 13:22:27 -0700202 return objectNode()
203 .put("id", member.id().toString())
204 .put("ip", member.ip().toString())
205 .put("online", member.isOnline())
206 .put("ready", member.isReady())
207 .put("uiAttached", isUiAttached)
Steven Burrowsad75aa22016-12-14 17:17:24 -0500208 .put("switches", switchCount);
Simon Huntd5b96732016-07-08 13:22:27 -0700209 }
210
211 /**
212 * Returns a JSON representation of the layout to use for displaying in
Simon Huntf836a872016-08-10 17:37:36 -0700213 * the topology view. The identifiers and names of regions from the
214 * current to the root is included, so that the bread-crumb widget can
215 * be rendered.
Simon Huntd5b96732016-07-08 13:22:27 -0700216 *
217 * @param layout the layout to transform
Simon Huntf836a872016-08-10 17:37:36 -0700218 * @param crumbs list of layouts in bread-crumb order
Simon Huntd5b96732016-07-08 13:22:27 -0700219 * @return a JSON representation of the data
220 */
Simon Huntf836a872016-08-10 17:37:36 -0700221 ObjectNode layout(UiTopoLayout layout, List<UiTopoLayout> crumbs) {
222 ObjectNode result = objectNode()
Simon Huntd5b96732016-07-08 13:22:27 -0700223 .put("id", layout.id().toString())
224 .put("parent", nullIsEmpty(layout.parent()))
225 .put("region", nullIsEmpty(layout.regionId()))
Simon Huntf836a872016-08-10 17:37:36 -0700226 .put("regionName", UiRegion.safeName(layout.region()));
227 addCrumbs(result, crumbs);
Simon Huntbc30e682017-02-15 18:39:23 -0800228 addBgRef(result, layout);
Simon Huntf836a872016-08-10 17:37:36 -0700229 return result;
Simon Huntd5b96732016-07-08 13:22:27 -0700230 }
231
Simon Huntbc30e682017-02-15 18:39:23 -0800232 private void addBgRef(ObjectNode result, UiTopoLayout layout) {
Steven Burrows86b74fc2017-02-22 00:15:16 +0000233 String mapId = layout.geomap();
234 String sprId = layout.sprites();
235
236 if (mapId != null) {
Simon Hunt95f4b422017-03-03 13:49:05 -0800237 result.put("bgType", GEO).put("bgId", mapId);
Steven Burrows86b74fc2017-02-22 00:15:16 +0000238 addMapParameters(result, mapId);
239 } else if (sprId != null) {
Simon Hunt95f4b422017-03-03 13:49:05 -0800240 result.put("bgType", GRID).put("bgId", sprId);
Steven Burrows86b74fc2017-02-22 00:15:16 +0000241 }
Simon Hunt95f4b422017-03-03 13:49:05 -0800242 addZoomPan(result, layout.id());
243 }
244
Steven Burrowsb43c1a92017-03-07 17:13:28 +0000245 private ObjectNode initialZoomForLayout(ObjectNode zoomPrefs, String id) {
246 ObjectNode zoomForLayout = defaultZoomForLayout();
247 zoomPrefs.set(id, zoomForLayout);
248 prefService.setPreference(userName, PKEY_TOPO_ZOOM, zoomPrefs);
249 return zoomPrefs;
250 }
251
252 private ObjectNode defaultZoomForLayout() {
253 return objectNode()
254 .put(ZOOM_SCALE, DEFAULT_SCALE)
255 .put(ZOOM_PAN_X, DEFAULT_PAN)
256 .put(ZOOM_PAN_Y, DEFAULT_PAN);
257 }
258
Simon Hunt95f4b422017-03-03 13:49:05 -0800259 private void addZoomPan(ObjectNode result, UiTopoLayoutId layoutId) {
260 // need to look up topo_zoom settings from preferences service.
261
262 // NOTE:
263 // UiPreferencesService API only allows us to retrieve ALL prefs for
264 // the given user. It would be better if we could call something like:
265 //
266 // ObjectNode value = prefService.getPreference(userName, prefKey);
267 //
268 // to get back a single value.
269
270 Map<String, ObjectNode> userPrefs = prefService.getPreferences(userName);
271 ObjectNode zoomPrefs = userPrefs.get(PKEY_TOPO_ZOOM);
272
273 if (zoomPrefs == null) {
Steven Burrowsb43c1a92017-03-07 17:13:28 +0000274 zoomPrefs = initialZoomForLayout(objectNode(), layoutId.id());
Simon Hunt95f4b422017-03-03 13:49:05 -0800275 }
Steven Burrowsb43c1a92017-03-07 17:13:28 +0000276
Simon Hunt95f4b422017-03-03 13:49:05 -0800277 ObjectNode zoomData = (ObjectNode) zoomPrefs.get(layoutId.id());
278
Steven Burrowsb43c1a92017-03-07 17:13:28 +0000279 if (zoomData == null) {
280 zoomPrefs = initialZoomForLayout(zoomPrefs, layoutId.id());
281 zoomData = (ObjectNode) zoomPrefs.get(layoutId.id());
282 }
283
Simon Hunt95f4b422017-03-03 13:49:05 -0800284 result.put("bgZoomScale", zoomData.get(ZOOM_SCALE).asText());
285 result.put("bgZoomPanX", zoomData.get(ZOOM_PAN_X).asText());
286 result.put("bgZoomPanY", zoomData.get(ZOOM_PAN_Y).asText());
Steven Burrows86b74fc2017-02-22 00:15:16 +0000287 }
288
289 private void addMapParameters(ObjectNode result, String mapId) {
290
291 // TODO: This ought to be written more efficiently.
292
293 // ALSO: Should retrieving a UiTopoMap by ID be something that
294 // the UiExtensionService provides, along with other
295 // useful lookups?
296 //
297 // Or should it remain very basic / general?
298 //
299 // return uiextService.getTopoMap(String mapId);
300
301 final UiTopoMap[] map = {null};
302
303 uiextService.getExtensions().forEach(ext -> {
304 UiTopoMapFactory factory = ext.topoMapFactory();
305
306 // TODO: use .stream().filter(...) here
307 if (map[0] == null && factory != null) {
308 List<UiTopoMap> topoMaps = factory.geoMaps();
309
310 topoMaps.forEach(m -> {
311 if (map[0] == null && m.id().equals(mapId)) {
312 map[0] = m;
313 }
314 });
315 }
316 });
317
318 UiTopoMap m = map[0];
319 if (m != null) {
320 result.put("bgDesc", m.description())
321 .put("bgFilePath", m.filePath())
322 .put("bgDefaultScale", m.scale());
323 } else {
324 result.put("bgWarn", "no map registered with id: " + mapId);
Simon Huntbc30e682017-02-15 18:39:23 -0800325 }
326 }
327
Simon Huntf836a872016-08-10 17:37:36 -0700328 private void addCrumbs(ObjectNode result, List<UiTopoLayout> crumbs) {
329 ArrayNode trail = arrayNode();
330 crumbs.forEach(c -> {
331 ObjectNode n = objectNode()
332 .put("id", c.regionId().toString())
333 .put("name", UiRegion.safeName(c.region()));
334 trail.add(n);
335 });
336 result.set("crumbs", trail);
Simon Huntd5b96732016-07-08 13:22:27 -0700337 }
338
339 /**
340 * Returns a JSON representation of the region to display in the topology
341 * view.
342 *
Simon Hunt977aa052016-07-20 17:08:29 -0700343 * @param region the region to transform to JSON
344 * @param subRegions the subregions within this region
Simon Huntc13082f2016-08-03 21:20:23 -0700345 * @param links the links within this region
Simon Huntd5b96732016-07-08 13:22:27 -0700346 * @return a JSON representation of the data
347 */
Simon Huntc13082f2016-08-03 21:20:23 -0700348 ObjectNode region(UiRegion region, Set<UiRegion> subRegions,
349 List<UiSynthLink> links) {
Simon Huntd5b96732016-07-08 13:22:27 -0700350 ObjectNode payload = objectNode();
Simon Huntd5b96732016-07-08 13:22:27 -0700351 if (region == null) {
352 payload.put("note", "no-region");
353 return payload;
354 }
Simon Hunt2521d5f2017-03-20 18:17:28 -0700355
356 String ridStr = region.idAsString();
357
358 payload.put("id", ridStr);
359 payload.set("subregions", jsonSubRegions(ridStr, subRegions));
Simon Huntcd508a62016-10-27 12:47:24 -0700360 payload.set("links", jsonLinks(links));
Simon Huntc13082f2016-08-03 21:20:23 -0700361
Simon Hunt977aa052016-07-20 17:08:29 -0700362 List<String> layerTags = region.layerOrder();
363 List<Set<UiNode>> splitDevices = splitByLayer(layerTags, region.devices());
364 List<Set<UiNode>> splitHosts = splitByLayer(layerTags, region.hosts());
Simon Huntd5b96732016-07-08 13:22:27 -0700365
Simon Hunt2521d5f2017-03-20 18:17:28 -0700366 payload.set("devices", jsonGrouped(ridStr, splitDevices));
367 payload.set("hosts", jsonGrouped(ridStr, splitHosts));
Simon Hunt977aa052016-07-20 17:08:29 -0700368 payload.set("layerOrder", jsonStrings(layerTags));
Simon Huntd5b96732016-07-08 13:22:27 -0700369
370 return payload;
371 }
372
Simon Hunt2521d5f2017-03-20 18:17:28 -0700373 private ArrayNode jsonSubRegions(String ridStr, Set<UiRegion> subregions) {
Simon Hunt977aa052016-07-20 17:08:29 -0700374 ArrayNode kids = arrayNode();
Simon Hunt2521d5f2017-03-20 18:17:28 -0700375 subregions.forEach(s -> kids.add(jsonClosedRegion(ridStr, s)));
Simon Hunt977aa052016-07-20 17:08:29 -0700376 return kids;
377 }
378
Simon Huntc13082f2016-08-03 21:20:23 -0700379 private JsonNode jsonLinks(List<UiSynthLink> links) {
380 ArrayNode synthLinks = arrayNode();
381 links.forEach(l -> synthLinks.add(json(l)));
382 return synthLinks;
383 }
384
Simon Hunt977aa052016-07-20 17:08:29 -0700385 private ArrayNode jsonStrings(List<String> strings) {
386 ArrayNode array = arrayNode();
387 strings.forEach(array::add);
388 return array;
389 }
390
Simon Hunt2521d5f2017-03-20 18:17:28 -0700391 private ArrayNode jsonGrouped(String ridStr, List<Set<UiNode>> groupedNodes) {
Simon Hunt977aa052016-07-20 17:08:29 -0700392 ArrayNode result = arrayNode();
393 groupedNodes.forEach(g -> {
394 ArrayNode subset = arrayNode();
Simon Hunt2521d5f2017-03-20 18:17:28 -0700395 g.forEach(n -> subset.add(json(ridStr, n)));
Simon Hunt977aa052016-07-20 17:08:29 -0700396 result.add(subset);
397 });
398 return result;
399 }
400
Simon Hunt537bc762016-12-20 12:15:13 -0800401 /**
Simon Hunt8eac4ae2017-01-20 12:56:45 -0800402 * Creates a JSON representation of a UI element.
403 *
404 * @param element the source element
405 * @return a JSON representation of that element
406 */
407 public ObjectNode jsonUiElement(UiElement element) {
408 if (element instanceof UiNode) {
Simon Hunt2521d5f2017-03-20 18:17:28 -0700409 return json(NO_CONTEXT, (UiNode) element);
Simon Hunt8eac4ae2017-01-20 12:56:45 -0800410 }
411 if (element instanceof UiLink) {
412 return json((UiLink) element);
413 }
414
415 // TODO: UiClusterMember
416
417 // Unrecognized UiElement class
418 return objectNode()
419 .put("warning", "unknown UiElement... cannot encode")
420 .put("javaclass", element.getClass().toString());
421 }
422
423 /**
Simon Hunt537bc762016-12-20 12:15:13 -0800424 * Creates a JSON representation of a UI model event.
425 *
426 * @param modelEvent the source model event
427 * @return a JSON representation of that event
428 */
429 public ObjectNode jsonEvent(UiModelEvent modelEvent) {
Steven Burrows512b6272016-12-19 14:09:45 -0500430 ObjectNode payload = objectNode();
431 payload.put(TYPE, enumToString(modelEvent.type()));
432 payload.put(SUBJECT, modelEvent.subject().idAsString());
Simon Hunt8eac4ae2017-01-20 12:56:45 -0800433 payload.set(DATA, modelEvent.data());
434 payload.put(MEMO, modelEvent.memo());
Steven Burrows512b6272016-12-19 14:09:45 -0500435 return payload;
436 }
437
438 // TODO: Investigate why we can't do this inline
439 private String enumToString(Enum<?> e) {
440 return e.toString();
441 }
442
Steven Burrowsad75aa22016-12-14 17:17:24 -0500443 // Returns the name of the master node for the specified device id.
444 private String master(DeviceId deviceId) {
445 NodeId master = mastershipService.getMasterFor(deviceId);
446 return master != null ? master.toString() : "";
447 }
Simon Hunt977aa052016-07-20 17:08:29 -0700448
Simon Hunt2521d5f2017-03-20 18:17:28 -0700449 private ObjectNode json(String ridStr, UiNode node) {
Simon Hunt977aa052016-07-20 17:08:29 -0700450 if (node instanceof UiRegion) {
Simon Hunt2521d5f2017-03-20 18:17:28 -0700451 return jsonClosedRegion(ridStr, (UiRegion) node);
Simon Hunt977aa052016-07-20 17:08:29 -0700452 }
453 if (node instanceof UiDevice) {
Simon Hunt2521d5f2017-03-20 18:17:28 -0700454 return json(ridStr, (UiDevice) node);
Simon Hunt977aa052016-07-20 17:08:29 -0700455 }
456 if (node instanceof UiHost) {
Simon Hunt2521d5f2017-03-20 18:17:28 -0700457 return json(ridStr, (UiHost) node);
Simon Hunt977aa052016-07-20 17:08:29 -0700458 }
459 throw new IllegalStateException(E_UNKNOWN_UI_NODE + node.getClass());
460 }
461
Simon Hunt2521d5f2017-03-20 18:17:28 -0700462 private ObjectNode json(String ridStr, UiDevice device) {
Simon Huntd5b96732016-07-08 13:22:27 -0700463 ObjectNode node = objectNode()
Simon Hunt977aa052016-07-20 17:08:29 -0700464 .put("id", device.idAsString())
Simon Hunt98189192016-07-29 19:02:27 -0700465 .put("nodeType", DEVICE)
Simon Huntd5b96732016-07-08 13:22:27 -0700466 .put("type", device.type())
Simon Hunt3d712522016-08-11 11:20:44 -0700467 .put("online", deviceService.isAvailable(device.id()))
Steven Burrowsad75aa22016-12-14 17:17:24 -0500468 .put("master", master(device.id()))
Simon Huntd5b96732016-07-08 13:22:27 -0700469 .put("layer", device.layer());
470
Simon Hunt6a8cb4f2016-08-09 15:08:57 -0700471 Device d = device.backingDevice();
472
473 addProps(node, d);
Simon Huntbc30e682017-02-15 18:39:23 -0800474 addGeoGridLocation(node, d);
Simon Hunt2521d5f2017-03-20 18:17:28 -0700475 addMetaUi(node, ridStr, device.idAsString());
Simon Huntd5b96732016-07-08 13:22:27 -0700476
477 return node;
478 }
479
Simon Hunt53612212016-12-04 17:19:52 -0800480 private void addProps(ObjectNode node, Annotated a) {
481 Annotations annot = a.annotations();
Simon Hunt6a8cb4f2016-08-09 15:08:57 -0700482 ObjectNode props = objectNode();
483 if (annot != null) {
484 annot.keys().forEach(k -> props.put(k, annot.value(k)));
485 }
486 node.set("props", props);
487 }
Simon Huntd5b96732016-07-08 13:22:27 -0700488
Simon Hunt2521d5f2017-03-20 18:17:28 -0700489 private void addMetaUi(ObjectNode node, String ridStr, String metaInstanceId) {
490 String key = contextKey(ridStr, metaInstanceId);
491 ObjectNode meta = metaUi.get(key);
Simon Hunt6a8cb4f2016-08-09 15:08:57 -0700492 if (meta != null) {
493 node.set("metaUi", meta);
494 }
495 }
496
Simon Huntbc30e682017-02-15 18:39:23 -0800497 private void addGeoGridLocation(ObjectNode node, Annotated a) {
Simon Hunt6a8cb4f2016-08-09 15:08:57 -0700498 List<String> lngLat = getAnnotValues(a, LONGITUDE, LATITUDE);
Simon Huntbc30e682017-02-15 18:39:23 -0800499 List<String> gridYX = getAnnotValues(a, GRID_Y, GRID_X);
Simon Hunt6a8cb4f2016-08-09 15:08:57 -0700500
Simon Huntbc30e682017-02-15 18:39:23 -0800501 if (lngLat != null) {
502 attachLocation(node, "geo", "lng", "lat", lngLat);
503 } else if (gridYX != null) {
504 attachLocation(node, "grid", "gridY", "gridX", gridYX);
505 }
506 }
507
508 private void attachLocation(ObjectNode node, String locType,
509 String keyA, String keyB, List<String> values) {
510 try {
511 double valA = Double.parseDouble(values.get(0));
512 double valB = Double.parseDouble(values.get(1));
513 ObjectNode loc = objectNode()
514 .put("type", locType)
515 .put(keyA, valA)
516 .put(keyB, valB);
517 node.set("location", loc);
518
519 } catch (NumberFormatException e) {
520 log.warn("Invalid {} data: long/Y={}, lat/X={}",
521 locType, values.get(0), values.get(1));
Simon Hunt6a8cb4f2016-08-09 15:08:57 -0700522 }
523 }
524
Steven Burrows583f4be2016-11-04 14:06:50 +0100525 private void addIps(ObjectNode node, Host h) {
526 Set<IpAddress> ips = h.ipAddresses();
527
528 ArrayNode a = arrayNode();
529 for (IpAddress ip : ips) {
530 a.add(ip.toString());
531 }
532
533 node.set("ips", a);
534 }
535
Simon Hunt6a8cb4f2016-08-09 15:08:57 -0700536 // return list of string values from annotated instance, for given keys
537 // return null if any keys are not present
538 List<String> getAnnotValues(Annotated a, String... annotKeys) {
539 List<String> result = new ArrayList<>(annotKeys.length);
540 for (String k : annotKeys) {
541 String v = a.annotations().value(k);
542 if (v == null) {
543 return null;
544 }
545 result.add(v);
546 }
547 return result;
548 }
549
550 // derive JSON object from annotations
551 private ObjectNode props(Annotations annotations) {
552 ObjectNode p = objectNode();
553 if (annotations != null) {
554 annotations.keys().forEach(k -> p.put(k, annotations.value(k)));
555 }
556 return p;
Simon Huntd5b96732016-07-08 13:22:27 -0700557 }
558
Simon Hunt2521d5f2017-03-20 18:17:28 -0700559 private ObjectNode json(String ridStr, UiHost host) {
Steven Burrows583f4be2016-11-04 14:06:50 +0100560 ObjectNode node = objectNode()
Simon Hunt977aa052016-07-20 17:08:29 -0700561 .put("id", host.idAsString())
Simon Hunt98189192016-07-29 19:02:27 -0700562 .put("nodeType", HOST)
Simon Huntd5b96732016-07-08 13:22:27 -0700563 .put("layer", host.layer());
564 // TODO: complete host details
Steven Burrows583f4be2016-11-04 14:06:50 +0100565 Host h = host.backingHost();
566
567 addIps(node, h);
Simon Huntbc30e682017-02-15 18:39:23 -0800568 addProps(node, h);
569 addGeoGridLocation(node, h);
Simon Hunt2521d5f2017-03-20 18:17:28 -0700570 addMetaUi(node, ridStr, host.idAsString());
Steven Burrows583f4be2016-11-04 14:06:50 +0100571
572 return node;
Simon Huntd5b96732016-07-08 13:22:27 -0700573 }
574
Simon Huntc13082f2016-08-03 21:20:23 -0700575 private ObjectNode json(UiSynthLink sLink) {
Simon Hunt8eac4ae2017-01-20 12:56:45 -0800576 return json(sLink.link());
577 }
578
579 private ObjectNode json(UiLink link) {
Simon Hunt3d712522016-08-11 11:20:44 -0700580 ObjectNode data = objectNode()
Simon Hunt8eac4ae2017-01-20 12:56:45 -0800581 .put("id", link.idAsString())
582 .put("epA", link.endPointA())
583 .put("epB", link.endPointB())
584 .put("type", link.type());
585 String pA = link.endPortA();
586 String pB = link.endPortB();
Simon Hunt3d712522016-08-11 11:20:44 -0700587 if (pA != null) {
588 data.put("portA", pA);
589 }
590 if (pB != null) {
591 data.put("portB", pB);
592 }
593 return data;
Simon Huntd5b96732016-07-08 13:22:27 -0700594 }
595
596
Simon Hunt2521d5f2017-03-20 18:17:28 -0700597 private ObjectNode jsonClosedRegion(String ridStr, UiRegion region) {
Steven Burrows482d9502016-09-27 11:24:58 -0700598 ObjectNode node = objectNode()
Simon Huntb1ce2602016-07-23 14:04:31 -0700599 .put("id", region.idAsString())
Simon Huntf836a872016-08-10 17:37:36 -0700600 .put("name", region.name())
Simon Hunt98189192016-07-29 19:02:27 -0700601 .put("nodeType", REGION)
Steven Burrows19e6e4f2016-10-05 13:27:07 -0500602 .put("nDevs", region.deviceCount())
603 .put("nHosts", region.hostCount());
Simon Hunt2521d5f2017-03-20 18:17:28 -0700604 // TODO: device and host counts should take into account any nested
605 // subregions. i.e. should be the sum of all devices/hosts in
606 // all descendent subregions.
Simon Hunt53612212016-12-04 17:19:52 -0800607
608 Region r = region.backingRegion();
Simon Hunt2521d5f2017-03-20 18:17:28 -0700609 // this is location data, as injected via network configuration script
Simon Huntbc30e682017-02-15 18:39:23 -0800610 addGeoGridLocation(node, r);
Simon Hunt53612212016-12-04 17:19:52 -0800611 addProps(node, r);
Steven Burrows482d9502016-09-27 11:24:58 -0700612
Simon Hunt2521d5f2017-03-20 18:17:28 -0700613 // this may contain location data, as dragged by user
614 // (which should take precedence, over configured data)
615 addMetaUi(node, ridStr, region.idAsString());
Steven Burrows482d9502016-09-27 11:24:58 -0700616 return node;
Simon Hunt977aa052016-07-20 17:08:29 -0700617 }
618
Simon Hunt98189192016-07-29 19:02:27 -0700619 /**
620 * Returns a JSON array representation of a set of regions/devices. Note
621 * that the information is sufficient for showing regions as nodes.
Simon Hunt2521d5f2017-03-20 18:17:28 -0700622 * THe region ID string defines the context (which region) the node is
623 * being displayed in.
Simon Hunt98189192016-07-29 19:02:27 -0700624 *
Simon Hunt2521d5f2017-03-20 18:17:28 -0700625 * @param ridStr region-id string
Simon Hunt98189192016-07-29 19:02:27 -0700626 * @param nodes the nodes
627 * @return a JSON representation of the nodes
628 */
Simon Hunt2521d5f2017-03-20 18:17:28 -0700629 public ArrayNode closedNodes(String ridStr, Set<UiNode> nodes) {
Simon Hunt98189192016-07-29 19:02:27 -0700630 ArrayNode array = arrayNode();
Simon Huntc13082f2016-08-03 21:20:23 -0700631 for (UiNode node : nodes) {
Simon Hunt98189192016-07-29 19:02:27 -0700632 if (node instanceof UiRegion) {
Simon Hunt2521d5f2017-03-20 18:17:28 -0700633 array.add(jsonClosedRegion(ridStr, (UiRegion) node));
Simon Hunt98189192016-07-29 19:02:27 -0700634 } else if (node instanceof UiDevice) {
Simon Hunt2521d5f2017-03-20 18:17:28 -0700635 array.add(json(ridStr, (UiDevice) node));
Simon Hunt98189192016-07-29 19:02:27 -0700636 } else {
637 log.warn("Unexpected node instance: {}", node.getClass());
638 }
639 }
640 return array;
641 }
Simon Hunt977aa052016-07-20 17:08:29 -0700642
Simon Hunt2521d5f2017-03-20 18:17:28 -0700643 // TODO: These methods do not seem to be used; consider removing them.
644 /*
Simon Hunt977aa052016-07-20 17:08:29 -0700645 * Returns a JSON array representation of a list of regions. Note that the
646 * information about each region is limited to what needs to be used to
647 * show the regions as nodes on the view.
648 *
649 * @param regions the regions
650 * @return a JSON representation of the minimal region information
651 */
Simon Hunt2521d5f2017-03-20 18:17:28 -0700652// public ArrayNode closedRegions(Set<UiRegion> regions) {
653// ArrayNode array = arrayNode();
654// for (UiRegion r : regions) {
655// array.add(jsonClosedRegion(r));
656// }
657// return array;
658// }
Simon Hunt977aa052016-07-20 17:08:29 -0700659
Simon Hunt2521d5f2017-03-20 18:17:28 -0700660 /*
Simon Hunt977aa052016-07-20 17:08:29 -0700661 * Returns a JSON array representation of a list of devices.
662 *
663 * @param devices the devices
664 * @return a JSON representation of the devices
665 */
Simon Hunt2521d5f2017-03-20 18:17:28 -0700666// public ArrayNode devices(Set<UiDevice> devices) {
667// ArrayNode array = arrayNode();
668// for (UiDevice device : devices) {
669// array.add(json(device));
670// }
671// return array;
672// }
Simon Hunt977aa052016-07-20 17:08:29 -0700673
Simon Hunt2521d5f2017-03-20 18:17:28 -0700674 /*
Simon Hunt977aa052016-07-20 17:08:29 -0700675 * Returns a JSON array representation of a list of hosts.
676 *
677 * @param hosts the hosts
678 * @return a JSON representation of the hosts
679 */
Simon Hunt2521d5f2017-03-20 18:17:28 -0700680// public ArrayNode hosts(Set<UiHost> hosts) {
681// ArrayNode array = arrayNode();
682// for (UiHost host : hosts) {
683// array.add(json(host));
684// }
685// return array;
686// }
Simon Hunt977aa052016-07-20 17:08:29 -0700687
Simon Hunt977aa052016-07-20 17:08:29 -0700688 // package-private for unit testing
689 List<Set<UiNode>> splitByLayer(List<String> layerTags,
690 Set<? extends UiNode> nodes) {
691 final int nLayers = layerTags.size();
692 if (!layerTags.get(nLayers - 1).equals(LAYER_DEFAULT)) {
693 throw new IllegalArgumentException(E_DEF_NOT_LAST);
694 }
695
696 List<Set<UiNode>> splitList = new ArrayList<>(layerTags.size());
697 Map<String, Set<UiNode>> byLayer = new HashMap<>(layerTags.size());
698
699 for (String tag : layerTags) {
700 Set<UiNode> set = new HashSet<>();
701 byLayer.put(tag, set);
702 splitList.add(set);
703 }
704
705 for (UiNode n : nodes) {
706 String which = n.layer();
707 if (!layerTags.contains(which)) {
708 which = LAYER_DEFAULT;
709 }
710 byLayer.get(which).add(n);
711 }
712
713 return splitList;
714 }
Steven Burrowse7cc3082016-09-27 11:24:58 -0700715
Simon Hunt2521d5f2017-03-20 18:17:28 -0700716
717 private String contextKey(String context, String key) {
718 return context + CONTEXT_KEY_DELIM + key;
719 }
720
Steven Burrowse7cc3082016-09-27 11:24:58 -0700721 /**
722 * Stores the memento for an element.
Simon Hunt2521d5f2017-03-20 18:17:28 -0700723 * This method assumes the payload has an id String, memento ObjectNode.
724 * The region-id string is used as a context within which to store the
725 * memento.
Steven Burrowse7cc3082016-09-27 11:24:58 -0700726 *
Simon Hunt2521d5f2017-03-20 18:17:28 -0700727 * @param ridStr region ID string
Steven Burrowse7cc3082016-09-27 11:24:58 -0700728 * @param payload event payload
729 */
Simon Hunt2521d5f2017-03-20 18:17:28 -0700730 void updateMeta(String ridStr, ObjectNode payload) {
Steven Burrowse7cc3082016-09-27 11:24:58 -0700731
732 String id = JsonUtils.string(payload, "id");
Simon Hunt2521d5f2017-03-20 18:17:28 -0700733 String key = contextKey(ridStr, id);
734 metaUi.put(key, JsonUtils.node(payload, "memento"));
Steven Burrowse7cc3082016-09-27 11:24:58 -0700735
Simon Hunt2521d5f2017-03-20 18:17:28 -0700736 log.debug("Storing metadata for {}", key);
Steven Burrowse7cc3082016-09-27 11:24:58 -0700737 }
Simon Huntd5b96732016-07-08 13:22:27 -0700738}