blob: 4ec61ce4d299f7dcba271dcddcff1ec198adf282 [file] [log] [blame]
Simon Huntd5b96732016-07-08 13:22:27 -07001/*
Brian O'Connora09fe5b2017-08-03 21:12:30 -07002 * Copyright 2016-present Open Networking Foundation
Simon Huntd5b96732016-07-08 13:22:27 -07003 *
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;
Simon Hunt708a5b32017-08-01 15:06:04 -070027import org.onosproject.cluster.ControllerNode;
Simon Huntd5b96732016-07-08 13:22:27 -070028import org.onosproject.cluster.NodeId;
29import org.onosproject.incubator.net.PortStatisticsService;
30import org.onosproject.incubator.net.tunnel.TunnelService;
31import org.onosproject.mastership.MastershipService;
Simon Hunt6a8cb4f2016-08-09 15:08:57 -070032import org.onosproject.net.Annotated;
33import org.onosproject.net.Annotations;
34import org.onosproject.net.Device;
Steven Burrowsad75aa22016-12-14 17:17:24 -050035import org.onosproject.net.DeviceId;
Steven Burrows583f4be2016-11-04 14:06:50 +010036import org.onosproject.net.Host;
Simon Huntd5b96732016-07-08 13:22:27 -070037import org.onosproject.net.device.DeviceService;
38import org.onosproject.net.flow.FlowRuleService;
39import org.onosproject.net.host.HostService;
40import org.onosproject.net.intent.IntentService;
41import org.onosproject.net.link.LinkService;
Simon Hunt53612212016-12-04 17:19:52 -080042import org.onosproject.net.region.Region;
Simon Huntd5b96732016-07-08 13:22:27 -070043import org.onosproject.net.statistic.StatisticService;
44import org.onosproject.net.topology.TopologyService;
Simon Hunt708a5b32017-08-01 15:06:04 -070045import org.onosproject.ui.GlyphConstants;
Steven Burrowse7cc3082016-09-27 11:24:58 -070046import org.onosproject.ui.JsonUtils;
Steven Burrows86b74fc2017-02-22 00:15:16 +000047import org.onosproject.ui.UiExtensionService;
Simon Hunt95f4b422017-03-03 13:49:05 -080048import org.onosproject.ui.UiPreferencesService;
Steven Burrows86b74fc2017-02-22 00:15:16 +000049import org.onosproject.ui.UiTopoMap;
50import org.onosproject.ui.UiTopoMapFactory;
Simon Huntd5b96732016-07-08 13:22:27 -070051import org.onosproject.ui.model.topo.UiClusterMember;
52import org.onosproject.ui.model.topo.UiDevice;
Simon Hunt8eac4ae2017-01-20 12:56:45 -080053import org.onosproject.ui.model.topo.UiElement;
Simon Huntd5b96732016-07-08 13:22:27 -070054import org.onosproject.ui.model.topo.UiHost;
55import org.onosproject.ui.model.topo.UiLink;
Simon Huntcf76a652017-05-12 18:28:24 -070056import org.onosproject.ui.model.topo.UiLinkId;
Simon Huntbf59db22017-05-12 13:26:35 -070057import org.onosproject.ui.model.topo.UiModelEvent;
Simon Hunt977aa052016-07-20 17:08:29 -070058import org.onosproject.ui.model.topo.UiNode;
Simon Huntd5b96732016-07-08 13:22:27 -070059import org.onosproject.ui.model.topo.UiRegion;
Simon Huntc13082f2016-08-03 21:20:23 -070060import org.onosproject.ui.model.topo.UiSynthLink;
Simon Huntd5b96732016-07-08 13:22:27 -070061import org.onosproject.ui.model.topo.UiTopoLayout;
Simon Hunt8f60ff82017-04-24 17:19:30 -070062import org.onosproject.ui.topo.LayoutLocation;
Simon Hunt98189192016-07-29 19:02:27 -070063import org.slf4j.Logger;
64import org.slf4j.LoggerFactory;
Simon Huntd5b96732016-07-08 13:22:27 -070065
Simon Hunt977aa052016-07-20 17:08:29 -070066import java.util.ArrayList;
67import java.util.HashMap;
68import java.util.HashSet;
Simon Huntd5b96732016-07-08 13:22:27 -070069import java.util.List;
Simon Hunt977aa052016-07-20 17:08:29 -070070import java.util.Map;
71import java.util.Set;
Simon Hunt6a8cb4f2016-08-09 15:08:57 -070072import java.util.concurrent.ConcurrentHashMap;
Simon Huntd5b96732016-07-08 13:22:27 -070073
74import static com.google.common.base.Preconditions.checkNotNull;
Simon Huntbc30e682017-02-15 18:39:23 -080075import static org.onosproject.net.AnnotationKeys.GRID_X;
76import static org.onosproject.net.AnnotationKeys.GRID_Y;
Simon Hunt6a8cb4f2016-08-09 15:08:57 -070077import static org.onosproject.net.AnnotationKeys.LATITUDE;
78import static org.onosproject.net.AnnotationKeys.LONGITUDE;
Simon Hunt977aa052016-07-20 17:08:29 -070079import static org.onosproject.ui.model.topo.UiNode.LAYER_DEFAULT;
Simon Hunt8f60ff82017-04-24 17:19:30 -070080import static org.onosproject.ui.topo.LayoutLocation.fromCompactListString;
Simon Huntd5b96732016-07-08 13:22:27 -070081
82/**
83 * Facility for creating JSON messages to send to the topology view in the
84 * Web client.
85 */
Simon Hunt537bc762016-12-20 12:15:13 -080086public class Topo2Jsonifier {
Simon Huntd5b96732016-07-08 13:22:27 -070087
Simon Hunt977aa052016-07-20 17:08:29 -070088 private static final String E_DEF_NOT_LAST =
89 "UiNode.LAYER_DEFAULT not last in layer list";
90 private static final String E_UNKNOWN_UI_NODE =
91 "Unknown subclass of UiNode: ";
92
Simon Hunt2521d5f2017-03-20 18:17:28 -070093 private static final String CONTEXT_KEY_DELIM = "_";
94 private static final String NO_CONTEXT = "";
Simon Huntf0c6f542017-03-22 18:31:18 -070095 private static final String ZOOM_KEY = "layoutZoom";
Simon Hunt2521d5f2017-03-20 18:17:28 -070096
Simon Hunt98189192016-07-29 19:02:27 -070097 private static final String REGION = "region";
98 private static final String DEVICE = "device";
99 private static final String HOST = "host";
Steven Burrows512b6272016-12-19 14:09:45 -0500100 private static final String TYPE = "type";
101 private static final String SUBJECT = "subject";
Simon Hunt8eac4ae2017-01-20 12:56:45 -0800102 private static final String DATA = "data";
103 private static final String MEMO = "memo";
Simon Hunt98189192016-07-29 19:02:27 -0700104
Simon Hunt95f4b422017-03-03 13:49:05 -0800105 private static final String GEO = "geo";
106 private static final String GRID = "grid";
Simon Hunt8f60ff82017-04-24 17:19:30 -0700107 private static final String PEER_LOCATIONS = "peerLocations";
Simon Huntf27a9292017-05-04 17:36:26 -0700108 private static final String LOCATION = "location";
109 private static final String LOC_TYPE = "locType";
110 private static final String LAT_OR_Y = "latOrY";
111 private static final String LONG_OR_X = "longOrX";
Simon Hunt95f4b422017-03-03 13:49:05 -0800112
Simon Hunt98189192016-07-29 19:02:27 -0700113 private final Logger log = LoggerFactory.getLogger(getClass());
114
Simon Huntd5b96732016-07-08 13:22:27 -0700115 private final ObjectMapper mapper = new ObjectMapper();
116
Simon Hunt95f4b422017-03-03 13:49:05 -0800117 // preferences are stored per user name...
118 private final String userName;
119
Simon Huntd5b96732016-07-08 13:22:27 -0700120 private ServiceDirectory directory;
121 private ClusterService clusterService;
122 private DeviceService deviceService;
123 private LinkService linkService;
124 private HostService hostService;
125 private MastershipService mastershipService;
126 private IntentService intentService;
127 private FlowRuleService flowService;
128 private StatisticService flowStatsService;
129 private PortStatisticsService portStatsService;
130 private TopologyService topologyService;
131 private TunnelService tunnelService;
Steven Burrows86b74fc2017-02-22 00:15:16 +0000132 private UiExtensionService uiextService;
Simon Hunt95f4b422017-03-03 13:49:05 -0800133 private UiPreferencesService prefService;
Simon Huntd5b96732016-07-08 13:22:27 -0700134
135
Simon Hunt6a8cb4f2016-08-09 15:08:57 -0700136 // NOTE: we'll stick this here for now, but maybe there is a better home?
137 // (this is not distributed across the cluster)
138 private static Map<String, ObjectNode> metaUi = new ConcurrentHashMap<>();
139
140
Simon Huntd5b96732016-07-08 13:22:27 -0700141 /**
142 * Creates an instance with a reference to the services directory, so that
143 * additional information about network elements may be looked up on
144 * on the fly.
145 *
146 * @param directory service directory
Simon Hunt95f4b422017-03-03 13:49:05 -0800147 * @param userName logged in user name
Simon Huntd5b96732016-07-08 13:22:27 -0700148 */
Simon Hunt95f4b422017-03-03 13:49:05 -0800149 public Topo2Jsonifier(ServiceDirectory directory, String userName) {
Simon Huntd5b96732016-07-08 13:22:27 -0700150 this.directory = checkNotNull(directory, "Directory cannot be null");
Simon Hunt95f4b422017-03-03 13:49:05 -0800151 this.userName = checkNotNull(userName, "User name cannot be null");
Simon Huntd5b96732016-07-08 13:22:27 -0700152
153 clusterService = directory.get(ClusterService.class);
154 deviceService = directory.get(DeviceService.class);
155 linkService = directory.get(LinkService.class);
156 hostService = directory.get(HostService.class);
157 mastershipService = directory.get(MastershipService.class);
158 intentService = directory.get(IntentService.class);
159 flowService = directory.get(FlowRuleService.class);
160 flowStatsService = directory.get(StatisticService.class);
161 portStatsService = directory.get(PortStatisticsService.class);
162 topologyService = directory.get(TopologyService.class);
163 tunnelService = directory.get(TunnelService.class);
Steven Burrows86b74fc2017-02-22 00:15:16 +0000164 uiextService = directory.get(UiExtensionService.class);
Simon Hunt95f4b422017-03-03 13:49:05 -0800165 prefService = directory.get(UiPreferencesService.class);
Simon Hunt977aa052016-07-20 17:08:29 -0700166 }
Simon Huntd5b96732016-07-08 13:22:27 -0700167
Simon Hunt977aa052016-07-20 17:08:29 -0700168 // for unit testing
169 Topo2Jsonifier() {
Simon Hunt95f4b422017-03-03 13:49:05 -0800170 userName = "(unit-test)";
Simon Huntd5b96732016-07-08 13:22:27 -0700171 }
172
173 private ObjectNode objectNode() {
174 return mapper.createObjectNode();
175 }
176
177 private ArrayNode arrayNode() {
178 return mapper.createArrayNode();
179 }
180
181 private String nullIsEmpty(Object o) {
182 return o == null ? "" : o.toString();
183 }
184
185
186 /**
187 * Returns a JSON representation of the cluster members (ONOS instances).
188 *
189 * @param instances the instance model objects
190 * @return a JSON representation of the data
191 */
192 ObjectNode instances(List<UiClusterMember> instances) {
193 NodeId local = clusterService.getLocalNode().id();
194 ObjectNode payload = objectNode();
195
196 ArrayNode members = arrayNode();
197 payload.set("members", members);
198 for (UiClusterMember member : instances) {
199 members.add(json(member, member.id().equals(local)));
200 }
201
202 return payload;
203 }
204
205 private ObjectNode json(UiClusterMember member, boolean isUiAttached) {
Steven Burrowsad75aa22016-12-14 17:17:24 -0500206 int switchCount = mastershipService.getDevicesOf(member.id()).size();
Simon Hunt708a5b32017-08-01 15:06:04 -0700207 ControllerNode.State state = clusterService.getState(member.id());
Simon Huntd5b96732016-07-08 13:22:27 -0700208 return objectNode()
209 .put("id", member.id().toString())
210 .put("ip", member.ip().toString())
Simon Hunt708a5b32017-08-01 15:06:04 -0700211 .put("online", state.isActive())
212 .put("ready", state.isReady())
Simon Huntd5b96732016-07-08 13:22:27 -0700213 .put("uiAttached", isUiAttached)
Steven Burrowsad75aa22016-12-14 17:17:24 -0500214 .put("switches", switchCount);
Simon Huntd5b96732016-07-08 13:22:27 -0700215 }
216
217 /**
218 * Returns a JSON representation of the layout to use for displaying in
Simon Huntf836a872016-08-10 17:37:36 -0700219 * the topology view. The identifiers and names of regions from the
220 * current to the root is included, so that the bread-crumb widget can
221 * be rendered.
Simon Huntd5b96732016-07-08 13:22:27 -0700222 *
223 * @param layout the layout to transform
Simon Huntf836a872016-08-10 17:37:36 -0700224 * @param crumbs list of layouts in bread-crumb order
Simon Huntd5b96732016-07-08 13:22:27 -0700225 * @return a JSON representation of the data
226 */
Simon Huntf7e7d4a2017-03-24 15:22:20 -0700227 ObjectNode layout(UiTopoLayout layout, List<UiTopoLayout> crumbs) {
Simon Huntf836a872016-08-10 17:37:36 -0700228 ObjectNode result = objectNode()
Simon Huntd5b96732016-07-08 13:22:27 -0700229 .put("id", layout.id().toString())
230 .put("parent", nullIsEmpty(layout.parent()))
231 .put("region", nullIsEmpty(layout.regionId()))
Simon Huntf836a872016-08-10 17:37:36 -0700232 .put("regionName", UiRegion.safeName(layout.region()));
233 addCrumbs(result, crumbs);
Simon Huntf7e7d4a2017-03-24 15:22:20 -0700234 addBgRef(result, layout);
Simon Huntf836a872016-08-10 17:37:36 -0700235 return result;
Simon Huntd5b96732016-07-08 13:22:27 -0700236 }
237
Simon Huntf7e7d4a2017-03-24 15:22:20 -0700238 private void addBgRef(ObjectNode result, UiTopoLayout layout) {
Steven Burrows86b74fc2017-02-22 00:15:16 +0000239 String mapId = layout.geomap();
240 String sprId = layout.sprites();
241
242 if (mapId != null) {
Simon Hunt95f4b422017-03-03 13:49:05 -0800243 result.put("bgType", GEO).put("bgId", mapId);
Steven Burrows86b74fc2017-02-22 00:15:16 +0000244 addMapParameters(result, mapId);
245 } else if (sprId != null) {
Simon Hunt95f4b422017-03-03 13:49:05 -0800246 result.put("bgType", GRID).put("bgId", sprId);
Steven Burrows86b74fc2017-02-22 00:15:16 +0000247 }
Simon Huntf0c6f542017-03-22 18:31:18 -0700248
Simon Huntf7e7d4a2017-03-24 15:22:20 -0700249 attachZoomData(result, layout);
Simon Hunt95f4b422017-03-03 13:49:05 -0800250 }
251
Simon Huntf7e7d4a2017-03-24 15:22:20 -0700252 private void attachZoomData(ObjectNode result, UiTopoLayout layout) {
Steven Burrowsb43c1a92017-03-07 17:13:28 +0000253
Simon Huntf0c6f542017-03-22 18:31:18 -0700254 ObjectNode zoomData = objectNode();
Steven Burrowsb43c1a92017-03-07 17:13:28 +0000255
Simon Huntf0c6f542017-03-22 18:31:18 -0700256 // first, set configured scale and offset
257 addCfgZoomData(zoomData, layout);
Simon Hunt95f4b422017-03-03 13:49:05 -0800258
Simon Huntf0c6f542017-03-22 18:31:18 -0700259 // next, retrieve user-set zoom data, if we have it
Simon Huntf7e7d4a2017-03-24 15:22:20 -0700260 String rid = layout.regionId().toString();
Simon Huntf0c6f542017-03-22 18:31:18 -0700261 ObjectNode userZoom = metaUi.get(contextKey(rid, ZOOM_KEY));
262 if (userZoom != null) {
263 zoomData.set("usr", userZoom);
Simon Hunt95f4b422017-03-03 13:49:05 -0800264 }
Simon Huntf0c6f542017-03-22 18:31:18 -0700265 result.set("bgZoom", zoomData);
266 }
Steven Burrowsb43c1a92017-03-07 17:13:28 +0000267
Simon Huntf0c6f542017-03-22 18:31:18 -0700268 private void addCfgZoomData(ObjectNode data, UiTopoLayout layout) {
269 ObjectNode zoom = objectNode();
270 zoom.put("scale", layout.scale());
271 zoom.put("offsetX", layout.offsetX());
272 zoom.put("offsetY", layout.offsetY());
273 data.set("cfg", zoom);
Steven Burrows86b74fc2017-02-22 00:15:16 +0000274 }
275
276 private void addMapParameters(ObjectNode result, String mapId) {
277
278 // TODO: This ought to be written more efficiently.
279
280 // ALSO: Should retrieving a UiTopoMap by ID be something that
281 // the UiExtensionService provides, along with other
282 // useful lookups?
283 //
284 // Or should it remain very basic / general?
285 //
286 // return uiextService.getTopoMap(String mapId);
287
288 final UiTopoMap[] map = {null};
289
290 uiextService.getExtensions().forEach(ext -> {
291 UiTopoMapFactory factory = ext.topoMapFactory();
292
293 // TODO: use .stream().filter(...) here
294 if (map[0] == null && factory != null) {
295 List<UiTopoMap> topoMaps = factory.geoMaps();
296
297 topoMaps.forEach(m -> {
298 if (map[0] == null && m.id().equals(mapId)) {
299 map[0] = m;
300 }
301 });
302 }
303 });
304
305 UiTopoMap m = map[0];
306 if (m != null) {
307 result.put("bgDesc", m.description())
308 .put("bgFilePath", m.filePath())
309 .put("bgDefaultScale", m.scale());
310 } else {
311 result.put("bgWarn", "no map registered with id: " + mapId);
Simon Huntbc30e682017-02-15 18:39:23 -0800312 }
313 }
314
Simon Huntf836a872016-08-10 17:37:36 -0700315 private void addCrumbs(ObjectNode result, List<UiTopoLayout> crumbs) {
316 ArrayNode trail = arrayNode();
317 crumbs.forEach(c -> {
318 ObjectNode n = objectNode()
319 .put("id", c.regionId().toString())
320 .put("name", UiRegion.safeName(c.region()));
321 trail.add(n);
322 });
323 result.set("crumbs", trail);
Simon Huntd5b96732016-07-08 13:22:27 -0700324 }
325
326 /**
327 * Returns a JSON representation of the region to display in the topology
328 * view.
329 *
Simon Hunt977aa052016-07-20 17:08:29 -0700330 * @param region the region to transform to JSON
331 * @param subRegions the subregions within this region
Simon Huntc13082f2016-08-03 21:20:23 -0700332 * @param links the links within this region
Simon Huntd5b96732016-07-08 13:22:27 -0700333 * @return a JSON representation of the data
334 */
Simon Huntc13082f2016-08-03 21:20:23 -0700335 ObjectNode region(UiRegion region, Set<UiRegion> subRegions,
336 List<UiSynthLink> links) {
Simon Huntd5b96732016-07-08 13:22:27 -0700337 ObjectNode payload = objectNode();
Simon Huntd5b96732016-07-08 13:22:27 -0700338 if (region == null) {
339 payload.put("note", "no-region");
340 return payload;
341 }
Simon Hunt2521d5f2017-03-20 18:17:28 -0700342
343 String ridStr = region.idAsString();
344
345 payload.put("id", ridStr);
346 payload.set("subregions", jsonSubRegions(ridStr, subRegions));
Simon Huntcd508a62016-10-27 12:47:24 -0700347 payload.set("links", jsonLinks(links));
Simon Huntc13082f2016-08-03 21:20:23 -0700348
Simon Hunt977aa052016-07-20 17:08:29 -0700349 List<String> layerTags = region.layerOrder();
350 List<Set<UiNode>> splitDevices = splitByLayer(layerTags, region.devices());
351 List<Set<UiNode>> splitHosts = splitByLayer(layerTags, region.hosts());
Simon Huntd5b96732016-07-08 13:22:27 -0700352
Simon Hunt2521d5f2017-03-20 18:17:28 -0700353 payload.set("devices", jsonGrouped(ridStr, splitDevices));
354 payload.set("hosts", jsonGrouped(ridStr, splitHosts));
Simon Hunt977aa052016-07-20 17:08:29 -0700355 payload.set("layerOrder", jsonStrings(layerTags));
Simon Huntd5b96732016-07-08 13:22:27 -0700356
Simon Hunt8f60ff82017-04-24 17:19:30 -0700357 if (!region.isRoot()) {
358 addPeerLocations(payload, region.backingRegion());
359 }
360
Simon Huntd5b96732016-07-08 13:22:27 -0700361 return payload;
362 }
363
Simon Hunt2521d5f2017-03-20 18:17:28 -0700364 private ArrayNode jsonSubRegions(String ridStr, Set<UiRegion> subregions) {
Simon Hunt977aa052016-07-20 17:08:29 -0700365 ArrayNode kids = arrayNode();
Simon Hunt2521d5f2017-03-20 18:17:28 -0700366 subregions.forEach(s -> kids.add(jsonClosedRegion(ridStr, s)));
Simon Hunt977aa052016-07-20 17:08:29 -0700367 return kids;
368 }
369
Simon Huntbf59db22017-05-12 13:26:35 -0700370 protected JsonNode jsonLinks(List<UiSynthLink> links) {
Simon Huntcf76a652017-05-12 18:28:24 -0700371 return collateSynthLinks(links);
Simon Huntc13082f2016-08-03 21:20:23 -0700372 }
373
Simon Hunt977aa052016-07-20 17:08:29 -0700374 private ArrayNode jsonStrings(List<String> strings) {
375 ArrayNode array = arrayNode();
376 strings.forEach(array::add);
377 return array;
378 }
379
Simon Hunt2521d5f2017-03-20 18:17:28 -0700380 private ArrayNode jsonGrouped(String ridStr, List<Set<UiNode>> groupedNodes) {
Simon Hunt977aa052016-07-20 17:08:29 -0700381 ArrayNode result = arrayNode();
382 groupedNodes.forEach(g -> {
383 ArrayNode subset = arrayNode();
Simon Hunt2521d5f2017-03-20 18:17:28 -0700384 g.forEach(n -> subset.add(json(ridStr, n)));
Simon Hunt977aa052016-07-20 17:08:29 -0700385 result.add(subset);
386 });
387 return result;
388 }
389
Simon Hunt537bc762016-12-20 12:15:13 -0800390 /**
Simon Hunt8eac4ae2017-01-20 12:56:45 -0800391 * Creates a JSON representation of a UI element.
392 *
393 * @param element the source element
394 * @return a JSON representation of that element
395 */
396 public ObjectNode jsonUiElement(UiElement element) {
397 if (element instanceof UiNode) {
Simon Hunt2521d5f2017-03-20 18:17:28 -0700398 return json(NO_CONTEXT, (UiNode) element);
Simon Hunt8eac4ae2017-01-20 12:56:45 -0800399 }
400 if (element instanceof UiLink) {
401 return json((UiLink) element);
402 }
Simon Hunt708a5b32017-08-01 15:06:04 -0700403 if (element instanceof UiClusterMember) {
404 return json((UiClusterMember) element);
405 }
Simon Hunt8eac4ae2017-01-20 12:56:45 -0800406
407 // Unrecognized UiElement class
408 return objectNode()
409 .put("warning", "unknown UiElement... cannot encode")
410 .put("javaclass", element.getClass().toString());
411 }
412
413 /**
Simon Hunt537bc762016-12-20 12:15:13 -0800414 * Creates a JSON representation of a UI model event.
415 *
416 * @param modelEvent the source model event
417 * @return a JSON representation of that event
418 */
419 public ObjectNode jsonEvent(UiModelEvent modelEvent) {
Steven Burrows512b6272016-12-19 14:09:45 -0500420 ObjectNode payload = objectNode();
421 payload.put(TYPE, enumToString(modelEvent.type()));
422 payload.put(SUBJECT, modelEvent.subject().idAsString());
Simon Hunt8eac4ae2017-01-20 12:56:45 -0800423 payload.set(DATA, modelEvent.data());
424 payload.put(MEMO, modelEvent.memo());
Steven Burrows512b6272016-12-19 14:09:45 -0500425 return payload;
426 }
427
428 // TODO: Investigate why we can't do this inline
429 private String enumToString(Enum<?> e) {
430 return e.toString();
431 }
432
Steven Burrowsad75aa22016-12-14 17:17:24 -0500433 // Returns the name of the master node for the specified device id.
434 private String master(DeviceId deviceId) {
435 NodeId master = mastershipService.getMasterFor(deviceId);
436 return master != null ? master.toString() : "";
437 }
Simon Hunt977aa052016-07-20 17:08:29 -0700438
Simon Hunt2521d5f2017-03-20 18:17:28 -0700439 private ObjectNode json(String ridStr, UiNode node) {
Simon Hunt977aa052016-07-20 17:08:29 -0700440 if (node instanceof UiRegion) {
Simon Hunt2521d5f2017-03-20 18:17:28 -0700441 return jsonClosedRegion(ridStr, (UiRegion) node);
Simon Hunt977aa052016-07-20 17:08:29 -0700442 }
443 if (node instanceof UiDevice) {
Simon Hunt2521d5f2017-03-20 18:17:28 -0700444 return json(ridStr, (UiDevice) node);
Simon Hunt977aa052016-07-20 17:08:29 -0700445 }
446 if (node instanceof UiHost) {
Simon Hunt2521d5f2017-03-20 18:17:28 -0700447 return json(ridStr, (UiHost) node);
Simon Hunt977aa052016-07-20 17:08:29 -0700448 }
449 throw new IllegalStateException(E_UNKNOWN_UI_NODE + node.getClass());
450 }
451
Simon Hunt2521d5f2017-03-20 18:17:28 -0700452 private ObjectNode json(String ridStr, UiDevice device) {
Simon Huntd5b96732016-07-08 13:22:27 -0700453 ObjectNode node = objectNode()
Simon Hunt977aa052016-07-20 17:08:29 -0700454 .put("id", device.idAsString())
Simon Hunt98189192016-07-29 19:02:27 -0700455 .put("nodeType", DEVICE)
Simon Huntd5b96732016-07-08 13:22:27 -0700456 .put("type", device.type())
Simon Hunt3d712522016-08-11 11:20:44 -0700457 .put("online", deviceService.isAvailable(device.id()))
Steven Burrowsad75aa22016-12-14 17:17:24 -0500458 .put("master", master(device.id()))
Simon Huntd5b96732016-07-08 13:22:27 -0700459 .put("layer", device.layer());
Simon Hunt6a8cb4f2016-08-09 15:08:57 -0700460 Device d = device.backingDevice();
dvaddireedeaf4a2017-06-21 00:00:30 +0530461 if (d != null) {
462 addProps(node, d);
463 addGeoGridLocation(node, d);
464 }
Simon Hunt2521d5f2017-03-20 18:17:28 -0700465 addMetaUi(node, ridStr, device.idAsString());
Simon Huntd5b96732016-07-08 13:22:27 -0700466
467 return node;
468 }
469
Simon Hunt53612212016-12-04 17:19:52 -0800470 private void addProps(ObjectNode node, Annotated a) {
471 Annotations annot = a.annotations();
Simon Hunt6a8cb4f2016-08-09 15:08:57 -0700472 ObjectNode props = objectNode();
473 if (annot != null) {
474 annot.keys().forEach(k -> props.put(k, annot.value(k)));
475 }
476 node.set("props", props);
477 }
Simon Huntd5b96732016-07-08 13:22:27 -0700478
Simon Hunt2521d5f2017-03-20 18:17:28 -0700479 private void addMetaUi(ObjectNode node, String ridStr, String metaInstanceId) {
480 String key = contextKey(ridStr, metaInstanceId);
481 ObjectNode meta = metaUi.get(key);
Simon Hunt6a8cb4f2016-08-09 15:08:57 -0700482 if (meta != null) {
483 node.set("metaUi", meta);
484 }
485 }
486
Simon Huntbc30e682017-02-15 18:39:23 -0800487 private void addGeoGridLocation(ObjectNode node, Annotated a) {
Simon Huntf27a9292017-05-04 17:36:26 -0700488 List<String> latLongData = getAnnotValues(a, LATITUDE, LONGITUDE);
489 List<String> gridYXdata = getAnnotValues(a, GRID_Y, GRID_X);
Simon Hunt6a8cb4f2016-08-09 15:08:57 -0700490
Simon Huntf27a9292017-05-04 17:36:26 -0700491 if (latLongData != null) {
492 attachLocation(node, GEO, latLongData);
493 } else if (gridYXdata != null) {
494 attachLocation(node, GRID, gridYXdata);
Simon Huntbc30e682017-02-15 18:39:23 -0800495 }
496 }
497
498 private void attachLocation(ObjectNode node, String locType,
Simon Huntf27a9292017-05-04 17:36:26 -0700499 List<String> values) {
Simon Huntbc30e682017-02-15 18:39:23 -0800500 try {
Simon Huntf27a9292017-05-04 17:36:26 -0700501 double latOrY = Double.parseDouble(values.get(0));
502 double longOrX = Double.parseDouble(values.get(1));
Simon Huntbc30e682017-02-15 18:39:23 -0800503 ObjectNode loc = objectNode()
Simon Huntf27a9292017-05-04 17:36:26 -0700504 .put(LOC_TYPE, locType)
505 .put(LAT_OR_Y, latOrY)
506 .put(LONG_OR_X, longOrX);
507 node.set(LOCATION, loc);
Simon Huntbc30e682017-02-15 18:39:23 -0800508
509 } catch (NumberFormatException e) {
Simon Huntf27a9292017-05-04 17:36:26 -0700510 log.warn("Invalid {} data: lat/Y={}, long/X={}",
Simon Huntbf59db22017-05-12 13:26:35 -0700511 locType, values.get(0), values.get(1));
Simon Hunt6a8cb4f2016-08-09 15:08:57 -0700512 }
513 }
514
Simon Hunt8f60ff82017-04-24 17:19:30 -0700515 private void addPeerLocations(ObjectNode node, Region r) {
516 String compact = r.annotations().value(PEER_LOCATIONS);
517 if (!Strings.isNullOrEmpty(compact)) {
518 List<LayoutLocation> locs = fromCompactListString(compact);
519
520 ObjectNode o = objectNode();
521 for (LayoutLocation ll : locs) {
522 ObjectNode lnode = objectNode()
Simon Huntbf59db22017-05-12 13:26:35 -0700523 .put(LOC_TYPE, ll.locType().toString())
524 .put(LAT_OR_Y, ll.latOrY())
525 .put(LONG_OR_X, ll.longOrX());
Simon Hunt8f60ff82017-04-24 17:19:30 -0700526 o.set(ll.id(), lnode);
527 }
528
529 node.set(PEER_LOCATIONS, o);
530 }
531 }
532
Steven Burrows583f4be2016-11-04 14:06:50 +0100533 private void addIps(ObjectNode node, Host h) {
534 Set<IpAddress> ips = h.ipAddresses();
535
536 ArrayNode a = arrayNode();
537 for (IpAddress ip : ips) {
538 a.add(ip.toString());
539 }
540
541 node.set("ips", a);
542 }
543
Simon Hunt6a8cb4f2016-08-09 15:08:57 -0700544 // return list of string values from annotated instance, for given keys
545 // return null if any keys are not present
546 List<String> getAnnotValues(Annotated a, String... annotKeys) {
547 List<String> result = new ArrayList<>(annotKeys.length);
548 for (String k : annotKeys) {
549 String v = a.annotations().value(k);
550 if (v == null) {
551 return null;
552 }
553 result.add(v);
554 }
555 return result;
556 }
557
558 // derive JSON object from annotations
559 private ObjectNode props(Annotations annotations) {
560 ObjectNode p = objectNode();
561 if (annotations != null) {
562 annotations.keys().forEach(k -> p.put(k, annotations.value(k)));
563 }
564 return p;
Simon Huntd5b96732016-07-08 13:22:27 -0700565 }
566
Simon Hunt2521d5f2017-03-20 18:17:28 -0700567 private ObjectNode json(String ridStr, UiHost host) {
Steven Burrows583f4be2016-11-04 14:06:50 +0100568 ObjectNode node = objectNode()
Simon Hunt977aa052016-07-20 17:08:29 -0700569 .put("id", host.idAsString())
Simon Hunt98189192016-07-29 19:02:27 -0700570 .put("nodeType", HOST)
Simon Huntd5b96732016-07-08 13:22:27 -0700571 .put("layer", host.layer());
572 // TODO: complete host details
Steven Burrows583f4be2016-11-04 14:06:50 +0100573 Host h = host.backingHost();
574
Simon Hunt5b83ac22017-05-16 10:53:43 -0700575 // h will be null, for example, after a HOST_REMOVED event
576 if (h != null) {
577 addIps(node, h);
578 addProps(node, h);
579 addGeoGridLocation(node, h);
Steven Burrows22cb5502018-02-06 11:28:33 +0000580 node.put("configured", h.configured());
Simon Hunt5b83ac22017-05-16 10:53:43 -0700581 }
Simon Hunt2521d5f2017-03-20 18:17:28 -0700582 addMetaUi(node, ridStr, host.idAsString());
Steven Burrows583f4be2016-11-04 14:06:50 +0100583
584 return node;
Simon Huntd5b96732016-07-08 13:22:27 -0700585 }
586
Simon Huntcf76a652017-05-12 18:28:24 -0700587 private ArrayNode collateSynthLinks(List<UiSynthLink> links) {
588 Map<UiLinkId, Set<UiSynthLink>> collation = new HashMap<>();
Simon Huntbf59db22017-05-12 13:26:35 -0700589
Simon Huntcf76a652017-05-12 18:28:24 -0700590 // first, group together the synthlinks into sets per ID...
591 for (UiSynthLink sl : links) {
592 UiLinkId id = sl.link().id();
593 Set<UiSynthLink> rollup =
594 collation.computeIfAbsent(id, k -> new HashSet<>());
595 rollup.add(sl);
596 }
597
598 // now add json nodes per set, and return the array of them
599 ArrayNode array = arrayNode();
600 for (UiLinkId id : collation.keySet()) {
601 array.add(json(collation.get(id)));
602 }
603 return array;
Simon Huntbf59db22017-05-12 13:26:35 -0700604 }
605
Simon Huntcf76a652017-05-12 18:28:24 -0700606 private ObjectNode json(Set<UiSynthLink> memberSet) {
607 ArrayNode rollup = arrayNode();
608 ObjectNode node = null;
609
610 boolean first = true;
611 for (UiSynthLink member : memberSet) {
612 UiLink link = member.link();
613 if (first) {
614 node = json(link);
615 first = false;
616 }
617 rollup.add(json(member.original()));
618 }
619 if (node != null) {
620 node.set("rollup", rollup);
621 }
622 return node;
Simon Hunt8eac4ae2017-01-20 12:56:45 -0800623 }
624
625 private ObjectNode json(UiLink link) {
Simon Hunt3d712522016-08-11 11:20:44 -0700626 ObjectNode data = objectNode()
Simon Hunt8eac4ae2017-01-20 12:56:45 -0800627 .put("id", link.idAsString())
628 .put("epA", link.endPointA())
629 .put("epB", link.endPointB())
630 .put("type", link.type());
631 String pA = link.endPortA();
632 String pB = link.endPortB();
Simon Hunt3d712522016-08-11 11:20:44 -0700633 if (pA != null) {
634 data.put("portA", pA);
635 }
636 if (pB != null) {
637 data.put("portB", pB);
638 }
639 return data;
Simon Huntd5b96732016-07-08 13:22:27 -0700640 }
641
Simon Hunt708a5b32017-08-01 15:06:04 -0700642 private ObjectNode json(UiClusterMember member) {
643 ControllerNode.State state = clusterService.getState(member.id());
644 return objectNode()
645 .put("id", member.idAsString())
646 .put("ip", member.ip().toString())
647 .put("online", state.isActive())
648 .put("ready", state.isReady())
649 .put(GlyphConstants.UI_ATTACHED,
650 member.backingNode().equals(clusterService.getLocalNode()));
651 }
Simon Huntd5b96732016-07-08 13:22:27 -0700652
Simon Hunt2521d5f2017-03-20 18:17:28 -0700653 private ObjectNode jsonClosedRegion(String ridStr, UiRegion region) {
Steven Burrows482d9502016-09-27 11:24:58 -0700654 ObjectNode node = objectNode()
Simon Huntb1ce2602016-07-23 14:04:31 -0700655 .put("id", region.idAsString())
Simon Huntf836a872016-08-10 17:37:36 -0700656 .put("name", region.name())
Simon Hunt98189192016-07-29 19:02:27 -0700657 .put("nodeType", REGION)
Steven Burrows19e6e4f2016-10-05 13:27:07 -0500658 .put("nDevs", region.deviceCount())
659 .put("nHosts", region.hostCount());
Simon Hunt2521d5f2017-03-20 18:17:28 -0700660 // TODO: device and host counts should take into account any nested
661 // subregions. i.e. should be the sum of all devices/hosts in
Simon Hunt8f60ff82017-04-24 17:19:30 -0700662 // all descendant subregions.
Simon Hunt53612212016-12-04 17:19:52 -0800663
664 Region r = region.backingRegion();
Simon Hunt0ee20bf2017-05-10 19:59:17 -0700665 if (r != null) {
666 // add data injected via network configuration script
667 addGeoGridLocation(node, r);
668 addProps(node, r);
669 }
Steven Burrows482d9502016-09-27 11:24:58 -0700670
Simon Hunt2521d5f2017-03-20 18:17:28 -0700671 // this may contain location data, as dragged by user
672 // (which should take precedence, over configured data)
673 addMetaUi(node, ridStr, region.idAsString());
Steven Burrows482d9502016-09-27 11:24:58 -0700674 return node;
Simon Hunt977aa052016-07-20 17:08:29 -0700675 }
676
Simon Hunt98189192016-07-29 19:02:27 -0700677 /**
678 * Returns a JSON array representation of a set of regions/devices. Note
679 * that the information is sufficient for showing regions as nodes.
Simon Hunt2521d5f2017-03-20 18:17:28 -0700680 * THe region ID string defines the context (which region) the node is
681 * being displayed in.
Simon Hunt98189192016-07-29 19:02:27 -0700682 *
Simon Hunt2521d5f2017-03-20 18:17:28 -0700683 * @param ridStr region-id string
Simon Huntf0c6f542017-03-22 18:31:18 -0700684 * @param nodes the nodes
Simon Hunt98189192016-07-29 19:02:27 -0700685 * @return a JSON representation of the nodes
686 */
Simon Hunt2521d5f2017-03-20 18:17:28 -0700687 public ArrayNode closedNodes(String ridStr, Set<UiNode> nodes) {
Simon Hunt98189192016-07-29 19:02:27 -0700688 ArrayNode array = arrayNode();
Simon Huntc13082f2016-08-03 21:20:23 -0700689 for (UiNode node : nodes) {
Simon Hunt98189192016-07-29 19:02:27 -0700690 if (node instanceof UiRegion) {
Simon Hunt2521d5f2017-03-20 18:17:28 -0700691 array.add(jsonClosedRegion(ridStr, (UiRegion) node));
Simon Hunt98189192016-07-29 19:02:27 -0700692 } else if (node instanceof UiDevice) {
Simon Hunt2521d5f2017-03-20 18:17:28 -0700693 array.add(json(ridStr, (UiDevice) node));
Simon Hunt98189192016-07-29 19:02:27 -0700694 } else {
695 log.warn("Unexpected node instance: {}", node.getClass());
696 }
697 }
698 return array;
699 }
Simon Hunt977aa052016-07-20 17:08:29 -0700700
Simon Hunt977aa052016-07-20 17:08:29 -0700701 // package-private for unit testing
702 List<Set<UiNode>> splitByLayer(List<String> layerTags,
703 Set<? extends UiNode> nodes) {
704 final int nLayers = layerTags.size();
705 if (!layerTags.get(nLayers - 1).equals(LAYER_DEFAULT)) {
706 throw new IllegalArgumentException(E_DEF_NOT_LAST);
707 }
708
709 List<Set<UiNode>> splitList = new ArrayList<>(layerTags.size());
710 Map<String, Set<UiNode>> byLayer = new HashMap<>(layerTags.size());
711
712 for (String tag : layerTags) {
713 Set<UiNode> set = new HashSet<>();
714 byLayer.put(tag, set);
715 splitList.add(set);
716 }
717
718 for (UiNode n : nodes) {
719 String which = n.layer();
720 if (!layerTags.contains(which)) {
721 which = LAYER_DEFAULT;
722 }
723 byLayer.get(which).add(n);
724 }
725
726 return splitList;
727 }
Steven Burrowse7cc3082016-09-27 11:24:58 -0700728
Simon Hunt2521d5f2017-03-20 18:17:28 -0700729
730 private String contextKey(String context, String key) {
731 return context + CONTEXT_KEY_DELIM + key;
732 }
733
Steven Burrowse7cc3082016-09-27 11:24:58 -0700734 /**
735 * Stores the memento for an element.
Simon Hunt2521d5f2017-03-20 18:17:28 -0700736 * This method assumes the payload has an id String, memento ObjectNode.
737 * The region-id string is used as a context within which to store the
738 * memento.
Steven Burrowse7cc3082016-09-27 11:24:58 -0700739 *
Simon Hunt2521d5f2017-03-20 18:17:28 -0700740 * @param ridStr region ID string
Steven Burrowse7cc3082016-09-27 11:24:58 -0700741 * @param payload event payload
742 */
Simon Hunt2521d5f2017-03-20 18:17:28 -0700743 void updateMeta(String ridStr, ObjectNode payload) {
Steven Burrowse7cc3082016-09-27 11:24:58 -0700744
745 String id = JsonUtils.string(payload, "id");
Simon Hunt2521d5f2017-03-20 18:17:28 -0700746 String key = contextKey(ridStr, id);
747 metaUi.put(key, JsonUtils.node(payload, "memento"));
Steven Burrowse7cc3082016-09-27 11:24:58 -0700748
Simon Hunt2521d5f2017-03-20 18:17:28 -0700749 log.debug("Storing metadata for {}", key);
Steven Burrowse7cc3082016-09-27 11:24:58 -0700750 }
Simon Huntd5b96732016-07-08 13:22:27 -0700751}