blob: 8b5741a41211f855253a00b84eaccda1bc3aeb6d [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;
Simon Hunt8f60ff82017-04-24 17:19:30 -070023import com.google.common.base.Strings;
Simon Huntd5b96732016-07-08 13:22:27 -070024import org.onlab.osgi.ServiceDirectory;
Steven Burrows583f4be2016-11-04 14:06:50 +010025import org.onlab.packet.IpAddress;
Simon Huntd5b96732016-07-08 13:22:27 -070026import org.onosproject.cluster.ClusterService;
27import org.onosproject.cluster.NodeId;
28import org.onosproject.incubator.net.PortStatisticsService;
29import org.onosproject.incubator.net.tunnel.TunnelService;
30import org.onosproject.mastership.MastershipService;
Simon Hunt6a8cb4f2016-08-09 15:08:57 -070031import org.onosproject.net.Annotated;
32import org.onosproject.net.Annotations;
33import org.onosproject.net.Device;
Steven Burrowsad75aa22016-12-14 17:17:24 -050034import org.onosproject.net.DeviceId;
Steven Burrows583f4be2016-11-04 14:06:50 +010035import org.onosproject.net.Host;
Simon Huntd5b96732016-07-08 13:22:27 -070036import org.onosproject.net.device.DeviceService;
37import org.onosproject.net.flow.FlowRuleService;
38import org.onosproject.net.host.HostService;
39import org.onosproject.net.intent.IntentService;
40import org.onosproject.net.link.LinkService;
Simon Hunt53612212016-12-04 17:19:52 -080041import org.onosproject.net.region.Region;
Simon Huntd5b96732016-07-08 13:22:27 -070042import org.onosproject.net.statistic.StatisticService;
43import org.onosproject.net.topology.TopologyService;
Steven Burrowse7cc3082016-09-27 11:24:58 -070044import org.onosproject.ui.JsonUtils;
Steven Burrows86b74fc2017-02-22 00:15:16 +000045import org.onosproject.ui.UiExtensionService;
Simon Hunt95f4b422017-03-03 13:49:05 -080046import org.onosproject.ui.UiPreferencesService;
Steven Burrows86b74fc2017-02-22 00:15:16 +000047import org.onosproject.ui.UiTopoMap;
48import org.onosproject.ui.UiTopoMapFactory;
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 Huntbf59db22017-05-12 13:26:35 -070054import org.onosproject.ui.model.topo.UiModelEvent;
Simon Hunt977aa052016-07-20 17:08:29 -070055import org.onosproject.ui.model.topo.UiNode;
Simon Huntd5b96732016-07-08 13:22:27 -070056import org.onosproject.ui.model.topo.UiRegion;
Simon Huntc13082f2016-08-03 21:20:23 -070057import org.onosproject.ui.model.topo.UiSynthLink;
Simon Huntd5b96732016-07-08 13:22:27 -070058import org.onosproject.ui.model.topo.UiTopoLayout;
Simon Hunt8f60ff82017-04-24 17:19:30 -070059import org.onosproject.ui.topo.LayoutLocation;
Simon Hunt98189192016-07-29 19:02:27 -070060import org.slf4j.Logger;
61import org.slf4j.LoggerFactory;
Simon Huntd5b96732016-07-08 13:22:27 -070062
Simon Hunt977aa052016-07-20 17:08:29 -070063import java.util.ArrayList;
64import java.util.HashMap;
65import java.util.HashSet;
Simon Huntd5b96732016-07-08 13:22:27 -070066import java.util.List;
Simon Hunt977aa052016-07-20 17:08:29 -070067import java.util.Map;
68import java.util.Set;
Simon Hunt6a8cb4f2016-08-09 15:08:57 -070069import java.util.concurrent.ConcurrentHashMap;
Simon Huntd5b96732016-07-08 13:22:27 -070070
71import static com.google.common.base.Preconditions.checkNotNull;
Simon Huntbc30e682017-02-15 18:39:23 -080072import static org.onosproject.net.AnnotationKeys.GRID_X;
73import static org.onosproject.net.AnnotationKeys.GRID_Y;
Simon Hunt6a8cb4f2016-08-09 15:08:57 -070074import static org.onosproject.net.AnnotationKeys.LATITUDE;
75import static org.onosproject.net.AnnotationKeys.LONGITUDE;
Simon Hunt977aa052016-07-20 17:08:29 -070076import static org.onosproject.ui.model.topo.UiNode.LAYER_DEFAULT;
Simon Hunt8f60ff82017-04-24 17:19:30 -070077import static org.onosproject.ui.topo.LayoutLocation.fromCompactListString;
Simon Huntd5b96732016-07-08 13:22:27 -070078
79/**
80 * Facility for creating JSON messages to send to the topology view in the
81 * Web client.
82 */
Simon Hunt537bc762016-12-20 12:15:13 -080083public class Topo2Jsonifier {
Simon Huntd5b96732016-07-08 13:22:27 -070084
Simon Hunt977aa052016-07-20 17:08:29 -070085 private static final String E_DEF_NOT_LAST =
86 "UiNode.LAYER_DEFAULT not last in layer list";
87 private static final String E_UNKNOWN_UI_NODE =
88 "Unknown subclass of UiNode: ";
89
Simon Hunt2521d5f2017-03-20 18:17:28 -070090 private static final String CONTEXT_KEY_DELIM = "_";
91 private static final String NO_CONTEXT = "";
Simon Huntf0c6f542017-03-22 18:31:18 -070092 private static final String ZOOM_KEY = "layoutZoom";
Simon Hunt2521d5f2017-03-20 18:17:28 -070093
Simon Hunt98189192016-07-29 19:02:27 -070094 private static final String REGION = "region";
95 private static final String DEVICE = "device";
96 private static final String HOST = "host";
Steven Burrows512b6272016-12-19 14:09:45 -050097 private static final String TYPE = "type";
98 private static final String SUBJECT = "subject";
Simon Hunt8eac4ae2017-01-20 12:56:45 -080099 private static final String DATA = "data";
100 private static final String MEMO = "memo";
Simon Hunt98189192016-07-29 19:02:27 -0700101
Simon Hunt95f4b422017-03-03 13:49:05 -0800102 private static final String GEO = "geo";
103 private static final String GRID = "grid";
Simon Hunt8f60ff82017-04-24 17:19:30 -0700104 private static final String PEER_LOCATIONS = "peerLocations";
Simon Huntf27a9292017-05-04 17:36:26 -0700105 private static final String LOCATION = "location";
106 private static final String LOC_TYPE = "locType";
107 private static final String LAT_OR_Y = "latOrY";
108 private static final String LONG_OR_X = "longOrX";
Simon Hunt95f4b422017-03-03 13:49:05 -0800109
Simon Hunt98189192016-07-29 19:02:27 -0700110 private final Logger log = LoggerFactory.getLogger(getClass());
111
Simon Huntd5b96732016-07-08 13:22:27 -0700112 private final ObjectMapper mapper = new ObjectMapper();
113
Simon Hunt95f4b422017-03-03 13:49:05 -0800114 // preferences are stored per user name...
115 private final String userName;
116
Simon Huntd5b96732016-07-08 13:22:27 -0700117 private ServiceDirectory directory;
118 private ClusterService clusterService;
119 private DeviceService deviceService;
120 private LinkService linkService;
121 private HostService hostService;
122 private MastershipService mastershipService;
123 private IntentService intentService;
124 private FlowRuleService flowService;
125 private StatisticService flowStatsService;
126 private PortStatisticsService portStatsService;
127 private TopologyService topologyService;
128 private TunnelService tunnelService;
Steven Burrows86b74fc2017-02-22 00:15:16 +0000129 private UiExtensionService uiextService;
Simon Hunt95f4b422017-03-03 13:49:05 -0800130 private UiPreferencesService prefService;
Simon Huntd5b96732016-07-08 13:22:27 -0700131
132
Simon Hunt6a8cb4f2016-08-09 15:08:57 -0700133 // NOTE: we'll stick this here for now, but maybe there is a better home?
134 // (this is not distributed across the cluster)
135 private static Map<String, ObjectNode> metaUi = new ConcurrentHashMap<>();
136
137
Simon Huntd5b96732016-07-08 13:22:27 -0700138 /**
139 * Creates an instance with a reference to the services directory, so that
140 * additional information about network elements may be looked up on
141 * on the fly.
142 *
143 * @param directory service directory
Simon Hunt95f4b422017-03-03 13:49:05 -0800144 * @param userName logged in user name
Simon Huntd5b96732016-07-08 13:22:27 -0700145 */
Simon Hunt95f4b422017-03-03 13:49:05 -0800146 public Topo2Jsonifier(ServiceDirectory directory, String userName) {
Simon Huntd5b96732016-07-08 13:22:27 -0700147 this.directory = checkNotNull(directory, "Directory cannot be null");
Simon Hunt95f4b422017-03-03 13:49:05 -0800148 this.userName = checkNotNull(userName, "User name cannot be null");
Simon Huntd5b96732016-07-08 13:22:27 -0700149
150 clusterService = directory.get(ClusterService.class);
151 deviceService = directory.get(DeviceService.class);
152 linkService = directory.get(LinkService.class);
153 hostService = directory.get(HostService.class);
154 mastershipService = directory.get(MastershipService.class);
155 intentService = directory.get(IntentService.class);
156 flowService = directory.get(FlowRuleService.class);
157 flowStatsService = directory.get(StatisticService.class);
158 portStatsService = directory.get(PortStatisticsService.class);
159 topologyService = directory.get(TopologyService.class);
160 tunnelService = directory.get(TunnelService.class);
Steven Burrows86b74fc2017-02-22 00:15:16 +0000161 uiextService = directory.get(UiExtensionService.class);
Simon Hunt95f4b422017-03-03 13:49:05 -0800162 prefService = directory.get(UiPreferencesService.class);
Simon Hunt977aa052016-07-20 17:08:29 -0700163 }
Simon Huntd5b96732016-07-08 13:22:27 -0700164
Simon Hunt977aa052016-07-20 17:08:29 -0700165 // for unit testing
166 Topo2Jsonifier() {
Simon Hunt95f4b422017-03-03 13:49:05 -0800167 userName = "(unit-test)";
Simon Huntd5b96732016-07-08 13:22:27 -0700168 }
169
170 private ObjectNode objectNode() {
171 return mapper.createObjectNode();
172 }
173
174 private ArrayNode arrayNode() {
175 return mapper.createArrayNode();
176 }
177
178 private String nullIsEmpty(Object o) {
179 return o == null ? "" : o.toString();
180 }
181
182
183 /**
184 * Returns a JSON representation of the cluster members (ONOS instances).
185 *
186 * @param instances the instance model objects
187 * @return a JSON representation of the data
188 */
189 ObjectNode instances(List<UiClusterMember> instances) {
190 NodeId local = clusterService.getLocalNode().id();
191 ObjectNode payload = objectNode();
192
193 ArrayNode members = arrayNode();
194 payload.set("members", members);
195 for (UiClusterMember member : instances) {
196 members.add(json(member, member.id().equals(local)));
197 }
198
199 return payload;
200 }
201
202 private ObjectNode json(UiClusterMember member, boolean isUiAttached) {
Steven Burrowsad75aa22016-12-14 17:17:24 -0500203 int switchCount = mastershipService.getDevicesOf(member.id()).size();
Simon Huntd5b96732016-07-08 13:22:27 -0700204 return objectNode()
205 .put("id", member.id().toString())
206 .put("ip", member.ip().toString())
207 .put("online", member.isOnline())
208 .put("ready", member.isReady())
209 .put("uiAttached", isUiAttached)
Steven Burrowsad75aa22016-12-14 17:17:24 -0500210 .put("switches", switchCount);
Simon Huntd5b96732016-07-08 13:22:27 -0700211 }
212
213 /**
214 * Returns a JSON representation of the layout to use for displaying in
Simon Huntf836a872016-08-10 17:37:36 -0700215 * the topology view. The identifiers and names of regions from the
216 * current to the root is included, so that the bread-crumb widget can
217 * be rendered.
Simon Huntd5b96732016-07-08 13:22:27 -0700218 *
219 * @param layout the layout to transform
Simon Huntf836a872016-08-10 17:37:36 -0700220 * @param crumbs list of layouts in bread-crumb order
Simon Huntd5b96732016-07-08 13:22:27 -0700221 * @return a JSON representation of the data
222 */
Simon Huntf7e7d4a2017-03-24 15:22:20 -0700223 ObjectNode layout(UiTopoLayout layout, List<UiTopoLayout> crumbs) {
Simon Huntf836a872016-08-10 17:37:36 -0700224 ObjectNode result = objectNode()
Simon Huntd5b96732016-07-08 13:22:27 -0700225 .put("id", layout.id().toString())
226 .put("parent", nullIsEmpty(layout.parent()))
227 .put("region", nullIsEmpty(layout.regionId()))
Simon Huntf836a872016-08-10 17:37:36 -0700228 .put("regionName", UiRegion.safeName(layout.region()));
229 addCrumbs(result, crumbs);
Simon Huntf7e7d4a2017-03-24 15:22:20 -0700230 addBgRef(result, layout);
Simon Huntf836a872016-08-10 17:37:36 -0700231 return result;
Simon Huntd5b96732016-07-08 13:22:27 -0700232 }
233
Simon Huntf7e7d4a2017-03-24 15:22:20 -0700234 private void addBgRef(ObjectNode result, UiTopoLayout layout) {
Steven Burrows86b74fc2017-02-22 00:15:16 +0000235 String mapId = layout.geomap();
236 String sprId = layout.sprites();
237
238 if (mapId != null) {
Simon Hunt95f4b422017-03-03 13:49:05 -0800239 result.put("bgType", GEO).put("bgId", mapId);
Steven Burrows86b74fc2017-02-22 00:15:16 +0000240 addMapParameters(result, mapId);
241 } else if (sprId != null) {
Simon Hunt95f4b422017-03-03 13:49:05 -0800242 result.put("bgType", GRID).put("bgId", sprId);
Steven Burrows86b74fc2017-02-22 00:15:16 +0000243 }
Simon Huntf0c6f542017-03-22 18:31:18 -0700244
Simon Huntf7e7d4a2017-03-24 15:22:20 -0700245 attachZoomData(result, layout);
Simon Hunt95f4b422017-03-03 13:49:05 -0800246 }
247
Simon Huntf7e7d4a2017-03-24 15:22:20 -0700248 private void attachZoomData(ObjectNode result, UiTopoLayout layout) {
Steven Burrowsb43c1a92017-03-07 17:13:28 +0000249
Simon Huntf0c6f542017-03-22 18:31:18 -0700250 ObjectNode zoomData = objectNode();
Steven Burrowsb43c1a92017-03-07 17:13:28 +0000251
Simon Huntf0c6f542017-03-22 18:31:18 -0700252 // first, set configured scale and offset
253 addCfgZoomData(zoomData, layout);
Simon Hunt95f4b422017-03-03 13:49:05 -0800254
Simon Huntf0c6f542017-03-22 18:31:18 -0700255 // next, retrieve user-set zoom data, if we have it
Simon Huntf7e7d4a2017-03-24 15:22:20 -0700256 String rid = layout.regionId().toString();
Simon Huntf0c6f542017-03-22 18:31:18 -0700257 ObjectNode userZoom = metaUi.get(contextKey(rid, ZOOM_KEY));
258 if (userZoom != null) {
259 zoomData.set("usr", userZoom);
Simon Hunt95f4b422017-03-03 13:49:05 -0800260 }
Simon Huntf0c6f542017-03-22 18:31:18 -0700261 result.set("bgZoom", zoomData);
262 }
Steven Burrowsb43c1a92017-03-07 17:13:28 +0000263
Simon Huntf0c6f542017-03-22 18:31:18 -0700264 private void addCfgZoomData(ObjectNode data, UiTopoLayout layout) {
265 ObjectNode zoom = objectNode();
266 zoom.put("scale", layout.scale());
267 zoom.put("offsetX", layout.offsetX());
268 zoom.put("offsetY", layout.offsetY());
269 data.set("cfg", zoom);
Steven Burrows86b74fc2017-02-22 00:15:16 +0000270 }
271
272 private void addMapParameters(ObjectNode result, String mapId) {
273
274 // TODO: This ought to be written more efficiently.
275
276 // ALSO: Should retrieving a UiTopoMap by ID be something that
277 // the UiExtensionService provides, along with other
278 // useful lookups?
279 //
280 // Or should it remain very basic / general?
281 //
282 // return uiextService.getTopoMap(String mapId);
283
284 final UiTopoMap[] map = {null};
285
286 uiextService.getExtensions().forEach(ext -> {
287 UiTopoMapFactory factory = ext.topoMapFactory();
288
289 // TODO: use .stream().filter(...) here
290 if (map[0] == null && factory != null) {
291 List<UiTopoMap> topoMaps = factory.geoMaps();
292
293 topoMaps.forEach(m -> {
294 if (map[0] == null && m.id().equals(mapId)) {
295 map[0] = m;
296 }
297 });
298 }
299 });
300
301 UiTopoMap m = map[0];
302 if (m != null) {
303 result.put("bgDesc", m.description())
304 .put("bgFilePath", m.filePath())
305 .put("bgDefaultScale", m.scale());
306 } else {
307 result.put("bgWarn", "no map registered with id: " + mapId);
Simon Huntbc30e682017-02-15 18:39:23 -0800308 }
309 }
310
Simon Huntf836a872016-08-10 17:37:36 -0700311 private void addCrumbs(ObjectNode result, List<UiTopoLayout> crumbs) {
312 ArrayNode trail = arrayNode();
313 crumbs.forEach(c -> {
314 ObjectNode n = objectNode()
315 .put("id", c.regionId().toString())
316 .put("name", UiRegion.safeName(c.region()));
317 trail.add(n);
318 });
319 result.set("crumbs", trail);
Simon Huntd5b96732016-07-08 13:22:27 -0700320 }
321
322 /**
323 * Returns a JSON representation of the region to display in the topology
324 * view.
325 *
Simon Hunt977aa052016-07-20 17:08:29 -0700326 * @param region the region to transform to JSON
327 * @param subRegions the subregions within this region
Simon Huntc13082f2016-08-03 21:20:23 -0700328 * @param links the links within this region
Simon Huntd5b96732016-07-08 13:22:27 -0700329 * @return a JSON representation of the data
330 */
Simon Huntc13082f2016-08-03 21:20:23 -0700331 ObjectNode region(UiRegion region, Set<UiRegion> subRegions,
332 List<UiSynthLink> links) {
Simon Huntd5b96732016-07-08 13:22:27 -0700333 ObjectNode payload = objectNode();
Simon Huntd5b96732016-07-08 13:22:27 -0700334 if (region == null) {
335 payload.put("note", "no-region");
336 return payload;
337 }
Simon Hunt2521d5f2017-03-20 18:17:28 -0700338
339 String ridStr = region.idAsString();
340
341 payload.put("id", ridStr);
342 payload.set("subregions", jsonSubRegions(ridStr, subRegions));
Simon Huntcd508a62016-10-27 12:47:24 -0700343 payload.set("links", jsonLinks(links));
Simon Huntc13082f2016-08-03 21:20:23 -0700344
Simon Hunt977aa052016-07-20 17:08:29 -0700345 List<String> layerTags = region.layerOrder();
346 List<Set<UiNode>> splitDevices = splitByLayer(layerTags, region.devices());
347 List<Set<UiNode>> splitHosts = splitByLayer(layerTags, region.hosts());
Simon Huntd5b96732016-07-08 13:22:27 -0700348
Simon Hunt2521d5f2017-03-20 18:17:28 -0700349 payload.set("devices", jsonGrouped(ridStr, splitDevices));
350 payload.set("hosts", jsonGrouped(ridStr, splitHosts));
Simon Hunt977aa052016-07-20 17:08:29 -0700351 payload.set("layerOrder", jsonStrings(layerTags));
Simon Huntd5b96732016-07-08 13:22:27 -0700352
Simon Hunt8f60ff82017-04-24 17:19:30 -0700353 if (!region.isRoot()) {
354 addPeerLocations(payload, region.backingRegion());
355 }
356
Simon Huntd5b96732016-07-08 13:22:27 -0700357 return payload;
358 }
359
Simon Hunt2521d5f2017-03-20 18:17:28 -0700360 private ArrayNode jsonSubRegions(String ridStr, Set<UiRegion> subregions) {
Simon Hunt977aa052016-07-20 17:08:29 -0700361 ArrayNode kids = arrayNode();
Simon Hunt2521d5f2017-03-20 18:17:28 -0700362 subregions.forEach(s -> kids.add(jsonClosedRegion(ridStr, s)));
Simon Hunt977aa052016-07-20 17:08:29 -0700363 return kids;
364 }
365
Simon Huntbf59db22017-05-12 13:26:35 -0700366 protected JsonNode jsonLinks(List<UiSynthLink> links) {
Simon Huntc13082f2016-08-03 21:20:23 -0700367 ArrayNode synthLinks = arrayNode();
368 links.forEach(l -> synthLinks.add(json(l)));
Simon Huntbf59db22017-05-12 13:26:35 -0700369
370 // TODO: implement the following........
371// collateSynthLinks(synthLinks, links);
372
Simon Huntc13082f2016-08-03 21:20:23 -0700373 return synthLinks;
374 }
375
Simon Hunt977aa052016-07-20 17:08:29 -0700376 private ArrayNode jsonStrings(List<String> strings) {
377 ArrayNode array = arrayNode();
378 strings.forEach(array::add);
379 return array;
380 }
381
Simon Hunt2521d5f2017-03-20 18:17:28 -0700382 private ArrayNode jsonGrouped(String ridStr, List<Set<UiNode>> groupedNodes) {
Simon Hunt977aa052016-07-20 17:08:29 -0700383 ArrayNode result = arrayNode();
384 groupedNodes.forEach(g -> {
385 ArrayNode subset = arrayNode();
Simon Hunt2521d5f2017-03-20 18:17:28 -0700386 g.forEach(n -> subset.add(json(ridStr, n)));
Simon Hunt977aa052016-07-20 17:08:29 -0700387 result.add(subset);
388 });
389 return result;
390 }
391
Simon Hunt537bc762016-12-20 12:15:13 -0800392 /**
Simon Hunt8eac4ae2017-01-20 12:56:45 -0800393 * Creates a JSON representation of a UI element.
394 *
395 * @param element the source element
396 * @return a JSON representation of that element
397 */
398 public ObjectNode jsonUiElement(UiElement element) {
399 if (element instanceof UiNode) {
Simon Hunt2521d5f2017-03-20 18:17:28 -0700400 return json(NO_CONTEXT, (UiNode) element);
Simon Hunt8eac4ae2017-01-20 12:56:45 -0800401 }
402 if (element instanceof UiLink) {
403 return json((UiLink) element);
404 }
405
406 // TODO: UiClusterMember
407
408 // Unrecognized UiElement class
409 return objectNode()
410 .put("warning", "unknown UiElement... cannot encode")
411 .put("javaclass", element.getClass().toString());
412 }
413
414 /**
Simon Hunt537bc762016-12-20 12:15:13 -0800415 * Creates a JSON representation of a UI model event.
416 *
417 * @param modelEvent the source model event
418 * @return a JSON representation of that event
419 */
420 public ObjectNode jsonEvent(UiModelEvent modelEvent) {
Steven Burrows512b6272016-12-19 14:09:45 -0500421 ObjectNode payload = objectNode();
422 payload.put(TYPE, enumToString(modelEvent.type()));
423 payload.put(SUBJECT, modelEvent.subject().idAsString());
Simon Hunt8eac4ae2017-01-20 12:56:45 -0800424 payload.set(DATA, modelEvent.data());
425 payload.put(MEMO, modelEvent.memo());
Steven Burrows512b6272016-12-19 14:09:45 -0500426 return payload;
427 }
428
429 // TODO: Investigate why we can't do this inline
430 private String enumToString(Enum<?> e) {
431 return e.toString();
432 }
433
Steven Burrowsad75aa22016-12-14 17:17:24 -0500434 // Returns the name of the master node for the specified device id.
435 private String master(DeviceId deviceId) {
436 NodeId master = mastershipService.getMasterFor(deviceId);
437 return master != null ? master.toString() : "";
438 }
Simon Hunt977aa052016-07-20 17:08:29 -0700439
Simon Hunt2521d5f2017-03-20 18:17:28 -0700440 private ObjectNode json(String ridStr, UiNode node) {
Simon Hunt977aa052016-07-20 17:08:29 -0700441 if (node instanceof UiRegion) {
Simon Hunt2521d5f2017-03-20 18:17:28 -0700442 return jsonClosedRegion(ridStr, (UiRegion) node);
Simon Hunt977aa052016-07-20 17:08:29 -0700443 }
444 if (node instanceof UiDevice) {
Simon Hunt2521d5f2017-03-20 18:17:28 -0700445 return json(ridStr, (UiDevice) node);
Simon Hunt977aa052016-07-20 17:08:29 -0700446 }
447 if (node instanceof UiHost) {
Simon Hunt2521d5f2017-03-20 18:17:28 -0700448 return json(ridStr, (UiHost) node);
Simon Hunt977aa052016-07-20 17:08:29 -0700449 }
450 throw new IllegalStateException(E_UNKNOWN_UI_NODE + node.getClass());
451 }
452
Simon Hunt2521d5f2017-03-20 18:17:28 -0700453 private ObjectNode json(String ridStr, UiDevice device) {
Simon Huntd5b96732016-07-08 13:22:27 -0700454 ObjectNode node = objectNode()
Simon Hunt977aa052016-07-20 17:08:29 -0700455 .put("id", device.idAsString())
Simon Hunt98189192016-07-29 19:02:27 -0700456 .put("nodeType", DEVICE)
Simon Huntd5b96732016-07-08 13:22:27 -0700457 .put("type", device.type())
Simon Hunt3d712522016-08-11 11:20:44 -0700458 .put("online", deviceService.isAvailable(device.id()))
Steven Burrowsad75aa22016-12-14 17:17:24 -0500459 .put("master", master(device.id()))
Simon Huntd5b96732016-07-08 13:22:27 -0700460 .put("layer", device.layer());
461
Simon Hunt6a8cb4f2016-08-09 15:08:57 -0700462 Device d = device.backingDevice();
463
464 addProps(node, d);
Simon Huntbc30e682017-02-15 18:39:23 -0800465 addGeoGridLocation(node, d);
Simon Hunt2521d5f2017-03-20 18:17:28 -0700466 addMetaUi(node, ridStr, device.idAsString());
Simon Huntd5b96732016-07-08 13:22:27 -0700467
468 return node;
469 }
470
Simon Hunt53612212016-12-04 17:19:52 -0800471 private void addProps(ObjectNode node, Annotated a) {
472 Annotations annot = a.annotations();
Simon Hunt6a8cb4f2016-08-09 15:08:57 -0700473 ObjectNode props = objectNode();
474 if (annot != null) {
475 annot.keys().forEach(k -> props.put(k, annot.value(k)));
476 }
477 node.set("props", props);
478 }
Simon Huntd5b96732016-07-08 13:22:27 -0700479
Simon Hunt2521d5f2017-03-20 18:17:28 -0700480 private void addMetaUi(ObjectNode node, String ridStr, String metaInstanceId) {
481 String key = contextKey(ridStr, metaInstanceId);
482 ObjectNode meta = metaUi.get(key);
Simon Hunt6a8cb4f2016-08-09 15:08:57 -0700483 if (meta != null) {
484 node.set("metaUi", meta);
485 }
486 }
487
Simon Huntbc30e682017-02-15 18:39:23 -0800488 private void addGeoGridLocation(ObjectNode node, Annotated a) {
Simon Huntf27a9292017-05-04 17:36:26 -0700489 List<String> latLongData = getAnnotValues(a, LATITUDE, LONGITUDE);
490 List<String> gridYXdata = getAnnotValues(a, GRID_Y, GRID_X);
Simon Hunt6a8cb4f2016-08-09 15:08:57 -0700491
Simon Huntf27a9292017-05-04 17:36:26 -0700492 if (latLongData != null) {
493 attachLocation(node, GEO, latLongData);
494 } else if (gridYXdata != null) {
495 attachLocation(node, GRID, gridYXdata);
Simon Huntbc30e682017-02-15 18:39:23 -0800496 }
497 }
498
499 private void attachLocation(ObjectNode node, String locType,
Simon Huntf27a9292017-05-04 17:36:26 -0700500 List<String> values) {
Simon Huntbc30e682017-02-15 18:39:23 -0800501 try {
Simon Huntf27a9292017-05-04 17:36:26 -0700502 double latOrY = Double.parseDouble(values.get(0));
503 double longOrX = Double.parseDouble(values.get(1));
Simon Huntbc30e682017-02-15 18:39:23 -0800504 ObjectNode loc = objectNode()
Simon Huntf27a9292017-05-04 17:36:26 -0700505 .put(LOC_TYPE, locType)
506 .put(LAT_OR_Y, latOrY)
507 .put(LONG_OR_X, longOrX);
508 node.set(LOCATION, loc);
Simon Huntbc30e682017-02-15 18:39:23 -0800509
510 } catch (NumberFormatException e) {
Simon Huntf27a9292017-05-04 17:36:26 -0700511 log.warn("Invalid {} data: lat/Y={}, long/X={}",
Simon Huntbf59db22017-05-12 13:26:35 -0700512 locType, values.get(0), values.get(1));
Simon Hunt6a8cb4f2016-08-09 15:08:57 -0700513 }
514 }
515
Simon Hunt8f60ff82017-04-24 17:19:30 -0700516 private void addPeerLocations(ObjectNode node, Region r) {
517 String compact = r.annotations().value(PEER_LOCATIONS);
518 if (!Strings.isNullOrEmpty(compact)) {
519 List<LayoutLocation> locs = fromCompactListString(compact);
520
521 ObjectNode o = objectNode();
522 for (LayoutLocation ll : locs) {
523 ObjectNode lnode = objectNode()
Simon Huntbf59db22017-05-12 13:26:35 -0700524 .put(LOC_TYPE, ll.locType().toString())
525 .put(LAT_OR_Y, ll.latOrY())
526 .put(LONG_OR_X, ll.longOrX());
Simon Hunt8f60ff82017-04-24 17:19:30 -0700527 o.set(ll.id(), lnode);
528 }
529
530 node.set(PEER_LOCATIONS, o);
531 }
532 }
533
Steven Burrows583f4be2016-11-04 14:06:50 +0100534 private void addIps(ObjectNode node, Host h) {
535 Set<IpAddress> ips = h.ipAddresses();
536
537 ArrayNode a = arrayNode();
538 for (IpAddress ip : ips) {
539 a.add(ip.toString());
540 }
541
542 node.set("ips", a);
543 }
544
Simon Hunt6a8cb4f2016-08-09 15:08:57 -0700545 // return list of string values from annotated instance, for given keys
546 // return null if any keys are not present
547 List<String> getAnnotValues(Annotated a, String... annotKeys) {
548 List<String> result = new ArrayList<>(annotKeys.length);
549 for (String k : annotKeys) {
550 String v = a.annotations().value(k);
551 if (v == null) {
552 return null;
553 }
554 result.add(v);
555 }
556 return result;
557 }
558
559 // derive JSON object from annotations
560 private ObjectNode props(Annotations annotations) {
561 ObjectNode p = objectNode();
562 if (annotations != null) {
563 annotations.keys().forEach(k -> p.put(k, annotations.value(k)));
564 }
565 return p;
Simon Huntd5b96732016-07-08 13:22:27 -0700566 }
567
Simon Hunt2521d5f2017-03-20 18:17:28 -0700568 private ObjectNode json(String ridStr, UiHost host) {
Steven Burrows583f4be2016-11-04 14:06:50 +0100569 ObjectNode node = objectNode()
Simon Hunt977aa052016-07-20 17:08:29 -0700570 .put("id", host.idAsString())
Simon Hunt98189192016-07-29 19:02:27 -0700571 .put("nodeType", HOST)
Simon Huntd5b96732016-07-08 13:22:27 -0700572 .put("layer", host.layer());
573 // TODO: complete host details
Steven Burrows583f4be2016-11-04 14:06:50 +0100574 Host h = host.backingHost();
575
576 addIps(node, h);
Simon Huntbc30e682017-02-15 18:39:23 -0800577 addProps(node, h);
578 addGeoGridLocation(node, h);
Simon Hunt2521d5f2017-03-20 18:17:28 -0700579 addMetaUi(node, ridStr, host.idAsString());
Steven Burrows583f4be2016-11-04 14:06:50 +0100580
581 return node;
Simon Huntd5b96732016-07-08 13:22:27 -0700582 }
583
Simon Huntbf59db22017-05-12 13:26:35 -0700584 // TODO: add method to JSONify an aggregated collection of synth links
585
586 private void collateSynthLinks(ArrayNode array,
587 List<UiSynthLink> links) {
588 // TODO - combine via link id
589 }
590
Simon Huntc13082f2016-08-03 21:20:23 -0700591 private ObjectNode json(UiSynthLink sLink) {
Simon Hunt8eac4ae2017-01-20 12:56:45 -0800592 return json(sLink.link());
593 }
594
595 private ObjectNode json(UiLink link) {
Simon Hunt3d712522016-08-11 11:20:44 -0700596 ObjectNode data = objectNode()
Simon Hunt8eac4ae2017-01-20 12:56:45 -0800597 .put("id", link.idAsString())
598 .put("epA", link.endPointA())
599 .put("epB", link.endPointB())
600 .put("type", link.type());
601 String pA = link.endPortA();
602 String pB = link.endPortB();
Simon Hunt3d712522016-08-11 11:20:44 -0700603 if (pA != null) {
604 data.put("portA", pA);
605 }
606 if (pB != null) {
607 data.put("portB", pB);
608 }
609 return data;
Simon Huntd5b96732016-07-08 13:22:27 -0700610 }
611
612
Simon Hunt2521d5f2017-03-20 18:17:28 -0700613 private ObjectNode jsonClosedRegion(String ridStr, UiRegion region) {
Steven Burrows482d9502016-09-27 11:24:58 -0700614 ObjectNode node = objectNode()
Simon Huntb1ce2602016-07-23 14:04:31 -0700615 .put("id", region.idAsString())
Simon Huntf836a872016-08-10 17:37:36 -0700616 .put("name", region.name())
Simon Hunt98189192016-07-29 19:02:27 -0700617 .put("nodeType", REGION)
Steven Burrows19e6e4f2016-10-05 13:27:07 -0500618 .put("nDevs", region.deviceCount())
619 .put("nHosts", region.hostCount());
Simon Hunt2521d5f2017-03-20 18:17:28 -0700620 // TODO: device and host counts should take into account any nested
621 // subregions. i.e. should be the sum of all devices/hosts in
Simon Hunt8f60ff82017-04-24 17:19:30 -0700622 // all descendant subregions.
Simon Hunt53612212016-12-04 17:19:52 -0800623
624 Region r = region.backingRegion();
Simon Hunt0ee20bf2017-05-10 19:59:17 -0700625 if (r != null) {
626 // add data injected via network configuration script
627 addGeoGridLocation(node, r);
628 addProps(node, r);
629 }
Steven Burrows482d9502016-09-27 11:24:58 -0700630
Simon Hunt2521d5f2017-03-20 18:17:28 -0700631 // this may contain location data, as dragged by user
632 // (which should take precedence, over configured data)
633 addMetaUi(node, ridStr, region.idAsString());
Steven Burrows482d9502016-09-27 11:24:58 -0700634 return node;
Simon Hunt977aa052016-07-20 17:08:29 -0700635 }
636
Simon Hunt98189192016-07-29 19:02:27 -0700637 /**
638 * Returns a JSON array representation of a set of regions/devices. Note
639 * that the information is sufficient for showing regions as nodes.
Simon Hunt2521d5f2017-03-20 18:17:28 -0700640 * THe region ID string defines the context (which region) the node is
641 * being displayed in.
Simon Hunt98189192016-07-29 19:02:27 -0700642 *
Simon Hunt2521d5f2017-03-20 18:17:28 -0700643 * @param ridStr region-id string
Simon Huntf0c6f542017-03-22 18:31:18 -0700644 * @param nodes the nodes
Simon Hunt98189192016-07-29 19:02:27 -0700645 * @return a JSON representation of the nodes
646 */
Simon Hunt2521d5f2017-03-20 18:17:28 -0700647 public ArrayNode closedNodes(String ridStr, Set<UiNode> nodes) {
Simon Hunt98189192016-07-29 19:02:27 -0700648 ArrayNode array = arrayNode();
Simon Huntc13082f2016-08-03 21:20:23 -0700649 for (UiNode node : nodes) {
Simon Hunt98189192016-07-29 19:02:27 -0700650 if (node instanceof UiRegion) {
Simon Hunt2521d5f2017-03-20 18:17:28 -0700651 array.add(jsonClosedRegion(ridStr, (UiRegion) node));
Simon Hunt98189192016-07-29 19:02:27 -0700652 } else if (node instanceof UiDevice) {
Simon Hunt2521d5f2017-03-20 18:17:28 -0700653 array.add(json(ridStr, (UiDevice) node));
Simon Hunt98189192016-07-29 19:02:27 -0700654 } else {
655 log.warn("Unexpected node instance: {}", node.getClass());
656 }
657 }
658 return array;
659 }
Simon Hunt977aa052016-07-20 17:08:29 -0700660
Simon Hunt977aa052016-07-20 17:08:29 -0700661 // package-private for unit testing
662 List<Set<UiNode>> splitByLayer(List<String> layerTags,
663 Set<? extends UiNode> nodes) {
664 final int nLayers = layerTags.size();
665 if (!layerTags.get(nLayers - 1).equals(LAYER_DEFAULT)) {
666 throw new IllegalArgumentException(E_DEF_NOT_LAST);
667 }
668
669 List<Set<UiNode>> splitList = new ArrayList<>(layerTags.size());
670 Map<String, Set<UiNode>> byLayer = new HashMap<>(layerTags.size());
671
672 for (String tag : layerTags) {
673 Set<UiNode> set = new HashSet<>();
674 byLayer.put(tag, set);
675 splitList.add(set);
676 }
677
678 for (UiNode n : nodes) {
679 String which = n.layer();
680 if (!layerTags.contains(which)) {
681 which = LAYER_DEFAULT;
682 }
683 byLayer.get(which).add(n);
684 }
685
686 return splitList;
687 }
Steven Burrowse7cc3082016-09-27 11:24:58 -0700688
Simon Hunt2521d5f2017-03-20 18:17:28 -0700689
690 private String contextKey(String context, String key) {
691 return context + CONTEXT_KEY_DELIM + key;
692 }
693
Steven Burrowse7cc3082016-09-27 11:24:58 -0700694 /**
695 * Stores the memento for an element.
Simon Hunt2521d5f2017-03-20 18:17:28 -0700696 * This method assumes the payload has an id String, memento ObjectNode.
697 * The region-id string is used as a context within which to store the
698 * memento.
Steven Burrowse7cc3082016-09-27 11:24:58 -0700699 *
Simon Hunt2521d5f2017-03-20 18:17:28 -0700700 * @param ridStr region ID string
Steven Burrowse7cc3082016-09-27 11:24:58 -0700701 * @param payload event payload
702 */
Simon Hunt2521d5f2017-03-20 18:17:28 -0700703 void updateMeta(String ridStr, ObjectNode payload) {
Steven Burrowse7cc3082016-09-27 11:24:58 -0700704
705 String id = JsonUtils.string(payload, "id");
Simon Hunt2521d5f2017-03-20 18:17:28 -0700706 String key = contextKey(ridStr, id);
707 metaUi.put(key, JsonUtils.node(payload, "memento"));
Steven Burrowse7cc3082016-09-27 11:24:58 -0700708
Simon Hunt2521d5f2017-03-20 18:17:28 -0700709 log.debug("Storing metadata for {}", key);
Steven Burrowse7cc3082016-09-27 11:24:58 -0700710 }
Simon Huntd5b96732016-07-08 13:22:27 -0700711}