1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.apache.syncope.client.console.topology;
20
21 import de.agilecoders.wicket.core.markup.html.bootstrap.dialog.Modal;
22 import java.io.Serializable;
23 import java.net.URI;
24 import java.time.Duration;
25 import java.time.temporal.ChronoUnit;
26 import java.util.ArrayList;
27 import java.util.Collection;
28 import java.util.HashMap;
29 import java.util.HashSet;
30 import java.util.List;
31 import java.util.Locale;
32 import java.util.Map;
33 import java.util.Optional;
34 import java.util.Set;
35 import java.util.stream.Collectors;
36 import org.apache.commons.lang3.StringUtils;
37 import org.apache.commons.lang3.tuple.Pair;
38 import org.apache.cxf.jaxrs.client.WebClient;
39 import org.apache.syncope.client.console.SyncopeConsoleSession;
40 import org.apache.syncope.client.console.annotations.IdMPage;
41 import org.apache.syncope.client.console.pages.BasePage;
42 import org.apache.syncope.client.console.rest.ConnectorRestClient;
43 import org.apache.syncope.client.console.rest.ResourceRestClient;
44 import org.apache.syncope.client.console.wicket.markup.html.WebMarkupContainerNoVeil;
45 import org.apache.syncope.client.console.wicket.markup.html.bootstrap.dialog.BaseModal;
46 import org.apache.syncope.client.console.wicket.markup.html.form.ActionLink;
47 import org.apache.syncope.client.console.wicket.markup.html.form.ActionsPanel;
48 import org.apache.syncope.client.console.wizards.resources.AbstractResourceWizardBuilder.CreateEvent;
49 import org.apache.syncope.client.ui.commons.Constants;
50 import org.apache.syncope.common.lib.to.ConnInstanceTO;
51 import org.apache.syncope.common.lib.to.ResourceTO;
52 import org.apache.syncope.common.lib.types.IdMEntitlement;
53 import org.apache.syncope.common.rest.api.service.SyncopeService;
54 import org.apache.wicket.Component;
55 import org.apache.wicket.ajax.AbstractAjaxTimerBehavior;
56 import org.apache.wicket.ajax.AjaxEventBehavior;
57 import org.apache.wicket.ajax.AjaxRequestTarget;
58 import org.apache.wicket.behavior.Behavior;
59 import org.apache.wicket.event.IEvent;
60 import org.apache.wicket.markup.head.IHeaderResponse;
61 import org.apache.wicket.markup.head.OnDomReadyHeaderItem;
62 import org.apache.wicket.markup.html.WebMarkupContainer;
63 import org.apache.wicket.markup.html.list.ListItem;
64 import org.apache.wicket.markup.html.list.ListView;
65 import org.apache.wicket.model.LoadableDetachableModel;
66 import org.apache.wicket.spring.injection.annot.SpringBean;
67
68 @IdMPage(label = "Topology", icon = "fas fa-plug", listEntitlement = IdMEntitlement.RESOURCE_LIST, priority = 0)
69 public class Topology extends BasePage {
70
71 private static final long serialVersionUID = -1100228004207271272L;
72
73 public static final String CONNECTOR_SERVER_LOCATION_PREFIX = "connid://";
74
75 @SpringBean
76 protected ResourceRestClient resourceRestClient;
77
78 @SpringBean
79 protected ConnectorRestClient connectorRestClient;
80
81 protected final int origX = 3100;
82
83 protected final int origY = 2800;
84
85 protected final BaseModal<Serializable> modal;
86
87 protected final WebMarkupContainer newlyCreatedContainer;
88
89 protected final ListView<TopologyNode> newlyCreated;
90
91 protected final TopologyTogglePanel togglePanel;
92
93 protected final LoadableDetachableModel<List<ResourceTO>> resModel = new LoadableDetachableModel<>() {
94
95 private static final long serialVersionUID = 5275935387613157431L;
96
97 @Override
98 protected List<ResourceTO> load() {
99 return resourceRestClient.list();
100 }
101 };
102
103 protected final LoadableDetachableModel<Map<String, List<ConnInstanceTO>>> connModel =
104 new LoadableDetachableModel<>() {
105
106 private static final long serialVersionUID = 5275935387613157432L;
107
108 @Override
109 protected Map<String, List<ConnInstanceTO>> load() {
110 final Map<String, List<ConnInstanceTO>> res = new HashMap<>();
111
112 connectorRestClient.getAllConnectors().forEach(conn -> {
113 List<ConnInstanceTO> conns;
114 if (res.containsKey(conn.getLocation())) {
115 conns = res.get(conn.getLocation());
116 } else {
117 conns = new ArrayList<>();
118 res.put(conn.getLocation(), conns);
119 }
120 conns.add(conn);
121 });
122
123 return res;
124 }
125 };
126
127 protected final LoadableDetachableModel<Pair<List<URI>, List<URI>>> csModel =
128 new LoadableDetachableModel<>() {
129
130 private static final long serialVersionUID = 5275935387613157433L;
131
132 @Override
133 protected Pair<List<URI>, List<URI>> load() {
134 final List<URI> connectorServers = new ArrayList<>();
135 final List<URI> filePaths = new ArrayList<>();
136
137 SyncopeConsoleSession.get().getPlatformInfo().getConnIdLocations().forEach(location -> {
138 if (location.startsWith(CONNECTOR_SERVER_LOCATION_PREFIX)) {
139 connectorServers.add(URI.create(location));
140 } else {
141 filePaths.add(URI.create(location));
142 }
143 });
144
145 return Pair.of(connectorServers, filePaths);
146 }
147 };
148
149 protected enum SupportedOperation {
150
151 CHECK_RESOURCE,
152 CHECK_CONNECTOR,
153 ADD_ENDPOINT;
154
155 }
156
157 public Topology() {
158 modal = new BaseModal<>("resource-modal");
159 body.add(modal.size(Modal.Size.Large));
160 modal.setWindowClosedCallback(target -> modal.show(false));
161
162 TopologyWebSocketBehavior websocket = new TopologyWebSocketBehavior();
163 body.add(websocket);
164
165 togglePanel = new TopologyTogglePanel("toggle", getPageReference());
166 body.add(togglePanel);
167
168
169
170
171 ActionsPanel<Serializable> zoomActionPanel = new ActionsPanel<>("zoom", null);
172
173 zoomActionPanel.add(new ActionLink<>() {
174
175 private static final long serialVersionUID = -3722207913631435501L;
176
177 @Override
178 public void onClick(final AjaxRequestTarget target, final Serializable ignore) {
179 target.appendJavaScript("zoomIn($('#drawing')[0]);");
180 }
181 }, ActionLink.ActionType.ZOOM_IN, IdMEntitlement.CONNECTOR_LIST).disableIndicator().hideLabel();
182 zoomActionPanel.add(new ActionLink<>() {
183
184 private static final long serialVersionUID = -3722207913631435501L;
185
186 @Override
187 public void onClick(final AjaxRequestTarget target, final Serializable ignore) {
188 target.appendJavaScript("zoomOut($('#drawing')[0]);");
189 }
190 }, ActionLink.ActionType.ZOOM_OUT, IdMEntitlement.CONNECTOR_LIST).disableIndicator().hideLabel();
191
192 body.add(zoomActionPanel);
193
194
195
196
197
198 String rootName = StringUtils.capitalize(Constants.SYNCOPE);
199 final TopologyNode syncopeTopologyNode = new TopologyNode(rootName, rootName, TopologyNode.Kind.SYNCOPE);
200 syncopeTopologyNode.setX(origX);
201 syncopeTopologyNode.setY(origY);
202
203 URI uri = WebClient.client(SyncopeConsoleSession.get().getService(SyncopeService.class)).getBaseURI();
204 syncopeTopologyNode.setHost(uri.getHost());
205 syncopeTopologyNode.setPort(uri.getPort());
206
207 body.add(topologyNodePanel(Constants.SYNCOPE, syncopeTopologyNode, false));
208
209 Map<Serializable, Map<Serializable, TopologyNode>> connections = new HashMap<>();
210 Map<Serializable, TopologyNode> syncopeConnections = new HashMap<>();
211 connections.put(syncopeTopologyNode.getKey(), syncopeConnections);
212
213
214 Map<String, TopologyNode> servers = new HashMap<>();
215 Map<String, TopologyNode> connectors = new HashMap<>();
216
217
218
219
220
221 ListView<URI> connectorServers = new ListView<>("connectorServers", csModel.getObject().getLeft()) {
222
223 private static final long serialVersionUID = 6978621871488360380L;
224
225 private final int size = csModel.getObject().getLeft().size() + 1;
226
227 @Override
228 protected void populateItem(final ListItem<URI> item) {
229 int kx = size >= 4 ? 800 : (200 * size);
230
231 int x = (int) Math.round(origX + kx * Math.cos(Math.PI + Math.PI * (item.getIndex() + 1) / size));
232 int y = (int) Math.round(origY + 100 * Math.sin(Math.PI + Math.PI * (item.getIndex() + 1) / size));
233
234 URI location = item.getModelObject();
235 String url = location.toASCIIString();
236
237 TopologyNode topologynode = new TopologyNode(url, url, TopologyNode.Kind.CONNECTOR_SERVER);
238
239 topologynode.setHost(location.getHost());
240 topologynode.setPort(location.getPort());
241 topologynode.setX(x);
242 topologynode.setY(y);
243
244 servers.put(String.class.cast(topologynode.getKey()), topologynode);
245
246 item.add(topologyNodePanel("cs", topologynode, false));
247
248 syncopeConnections.put(url, topologynode);
249 connections.put(url, new HashMap<>());
250 }
251 };
252
253 connectorServers.setOutputMarkupId(true);
254 body.add(connectorServers);
255
256
257
258
259
260 ListView<URI> filePaths = new ListView<>("filePaths", csModel.getObject().getRight()) {
261
262 private static final long serialVersionUID = 6978621871488360380L;
263
264 private final int size = csModel.getObject().getRight().size() + 1;
265
266 @Override
267 protected void populateItem(final ListItem<URI> item) {
268 int kx = size >= 4 ? 800 : (200 * size);
269
270 int x = (int) Math.round(origX + kx * Math.cos(Math.PI * (item.getIndex() + 1) / size));
271 int y = (int) Math.round(origY + 100 * Math.sin(Math.PI * (item.getIndex() + 1) / size));
272
273 URI location = item.getModelObject();
274 String url = location.toASCIIString();
275
276 TopologyNode topologynode = new TopologyNode(url, url, TopologyNode.Kind.FS_PATH);
277
278 topologynode.setHost(location.getHost());
279 topologynode.setPort(location.getPort());
280 topologynode.setX(x);
281 topologynode.setY(y);
282
283 servers.put(String.class.cast(topologynode.getKey()), topologynode);
284
285 item.add(topologyNodePanel("fp", topologynode, false));
286
287 syncopeConnections.put(url, topologynode);
288 connections.put(url, new HashMap<>());
289 }
290 };
291
292 filePaths.setOutputMarkupId(true);
293 body.add(filePaths);
294
295
296
297
298
299 ListView<List<ConnInstanceTO>> conns =
300 new ListView<>("conns", new ArrayList<>(connModel.getObject().values())) {
301
302 private static final long serialVersionUID = 697862187148836036L;
303
304 @Override
305 protected void populateItem(final ListItem<List<ConnInstanceTO>> item) {
306 int size = item.getModelObject().size() + 1;
307
308 ListView<ConnInstanceTO> conns = new ListView<>("conns", item.getModelObject()) {
309
310 private static final long serialVersionUID = 6978621871488360381L;
311
312 @Override
313 protected void populateItem(final ListItem<ConnInstanceTO> item) {
314 ConnInstanceTO conn = item.getModelObject();
315
316 TopologyNode topologynode = new TopologyNode(
317 conn.getKey(),
318 StringUtils.isBlank(conn.getDisplayName())
319 ? conn.getBundleName() : conn.getDisplayName(),
320 TopologyNode.Kind.CONNECTOR);
321
322
323 TopologyNode parent = servers.get(conn.getLocation());
324
325
326 int kx = size >= 6 ? 800 : (130 * size);
327
328 double hpos = conn.getLocation().
329 startsWith(CONNECTOR_SERVER_LOCATION_PREFIX) ? Math.PI : 0.0;
330
331 int x = (int) Math.round((Optional.ofNullable(parent).map(TopologyNode::getX).orElse(origX))
332 + kx * Math.cos(hpos + Math.PI * (item.getIndex() + 1) / size));
333 int y = (int) Math.round((Optional.ofNullable(parent).map(TopologyNode::getY).orElse(origY))
334 + 100 * Math.sin(hpos + Math.PI * (item.getIndex() + 1) / size));
335
336 topologynode.setConnectionDisplayName(conn.getBundleName());
337 topologynode.setX(x);
338 topologynode.setY(y);
339
340 connectors.put(String.class.cast(topologynode.getKey()), topologynode);
341 item.add(topologyNodePanel("conn", topologynode, conn.isErrored()));
342
343
344 Map<Serializable, TopologyNode> remoteConnections;
345 if (connections.containsKey(conn.getLocation())) {
346 remoteConnections = connections.get(conn.getLocation());
347 } else {
348 remoteConnections = new HashMap<>();
349 connections.put(conn.getLocation(), remoteConnections);
350 }
351 remoteConnections.put(conn.getKey(), topologynode);
352 }
353 };
354
355 conns.setOutputMarkupId(true);
356 item.add(conns);
357 }
358 };
359
360 conns.setOutputMarkupId(true);
361 body.add(conns);
362
363
364
365
366
367 Collection<String> adminConns = new HashSet<>();
368 connModel.getObject().values().forEach(connInstances -> adminConns.addAll(
369 connInstances.stream().map(ConnInstanceTO::getKey).collect(Collectors.toList())));
370
371 Set<String> adminRes = new HashSet<>();
372 List<String> connToBeProcessed = new ArrayList<>();
373 resModel.getObject().stream().
374 filter(resourceTO -> adminConns.contains(resourceTO.getConnector())).
375 forEach(resourceTO -> {
376 TopologyNode topologynode = new TopologyNode(
377 resourceTO.getKey(), resourceTO.getKey(), TopologyNode.Kind.RESOURCE);
378
379 Map<Serializable, TopologyNode> remoteConnections;
380 if (connections.containsKey(resourceTO.getConnector())) {
381 remoteConnections = connections.get(resourceTO.getConnector());
382 } else {
383 remoteConnections = new HashMap<>();
384 connections.put(resourceTO.getConnector(), remoteConnections);
385 }
386 remoteConnections.put(topologynode.getKey(), topologynode);
387
388 adminRes.add(resourceTO.getKey());
389
390 if (!connToBeProcessed.contains(resourceTO.getConnector())) {
391 connToBeProcessed.add(resourceTO.getConnector());
392 }
393 });
394
395 ListView<String> resources = new ListView<>("resources", connToBeProcessed) {
396
397 private static final long serialVersionUID = 697862187148836038L;
398
399 @Override
400 protected void populateItem(final ListItem<String> item) {
401 String connectorKey = item.getModelObject();
402
403 ListView<TopologyNode> innerListView = new ListView<>("resources",
404 new ArrayList<>(connections.get(connectorKey).values())) {
405
406 private static final long serialVersionUID = -3447760771863754342L;
407
408 private final int size = getModelObject().size() + 1;
409
410 @Override
411 protected void populateItem(final ListItem<TopologyNode> item) {
412 TopologyNode topologynode = item.getModelObject();
413 TopologyNode parent = connectors.get(connectorKey);
414
415
416 int kx = size >= 16 ? 800 : (48 * size);
417 int ky = size < 4 ? 100 : size < 6 ? 350 : 750;
418
419 double hpos = (parent == null || parent.getY() < syncopeTopologyNode.getY()) ? Math.PI : 0.0;
420
421 int x = (int) Math.round((Optional.ofNullable(parent).map(TopologyNode::getX).orElse(origX))
422 + kx * Math.cos(hpos + Math.PI * (item.getIndex() + 1) / size));
423 int y = (int) Math.round((Optional.ofNullable(parent).map(TopologyNode::getY).orElse(origY))
424 + ky * Math.sin(hpos + Math.PI * (item.getIndex() + 1) / size));
425
426 topologynode.setX(x);
427 topologynode.setY(y);
428
429 item.add(topologyNodePanel("res", topologynode, false));
430 }
431 };
432
433 innerListView.setOutputMarkupId(true);
434 item.add(innerListView);
435 }
436 };
437
438 resources.setOutputMarkupId(true);
439 body.add(resources);
440
441
442
443
444
445 WebMarkupContainer jsPlace = new WebMarkupContainerNoVeil("jsPlace");
446 jsPlace.setOutputMarkupId(true);
447 body.add(jsPlace);
448
449 jsPlace.add(new Behavior() {
450
451 private static final long serialVersionUID = 2661717818979056044L;
452
453 @Override
454 public void renderHead(final Component component, final IHeaderResponse response) {
455 final StringBuilder jsPlumbConf = new StringBuilder();
456 jsPlumbConf.append(String.format(Locale.US, "activate(%.2f);", 0.68f));
457
458 createConnections(connections).forEach(jsPlumbConf::append);
459
460 response.render(OnDomReadyHeaderItem.forScript(jsPlumbConf.toString()));
461 }
462 });
463
464 jsPlace.add(new AbstractAjaxTimerBehavior(Duration.of(2, ChronoUnit.SECONDS)) {
465
466 private static final long serialVersionUID = -4426283634345968585L;
467
468 @Override
469 protected void onTimer(final AjaxRequestTarget target) {
470 if (websocket.connCheckDone(adminConns) && websocket.resCheckDone(adminRes)) {
471 stop(target);
472 }
473
474 target.appendJavaScript("checkConnection()");
475
476 if (getUpdateInterval().getSeconds() < 5.0) {
477 setUpdateInterval(Duration.of(5, ChronoUnit.SECONDS));
478 } else if (getUpdateInterval().getSeconds() < 10.0) {
479 setUpdateInterval(Duration.of(10, ChronoUnit.SECONDS));
480 } else if (getUpdateInterval().getSeconds() < 15.0) {
481 setUpdateInterval(Duration.of(15, ChronoUnit.SECONDS));
482 } else if (getUpdateInterval().getSeconds() < 20.0) {
483 setUpdateInterval(Duration.of(20, ChronoUnit.SECONDS));
484 } else if (getUpdateInterval().getSeconds() < 30.0) {
485 setUpdateInterval(Duration.of(30, ChronoUnit.SECONDS));
486 } else if (getUpdateInterval().getSeconds() < 60.0) {
487 setUpdateInterval(Duration.of(60, ChronoUnit.SECONDS));
488 }
489 }
490 });
491
492
493 newlyCreatedContainer = new WebMarkupContainer("newlyCreatedContainer");
494 newlyCreatedContainer.setOutputMarkupId(true);
495 body.add(newlyCreatedContainer);
496
497 newlyCreated = new ListView<>("newlyCreated", new ArrayList<>()) {
498
499 private static final long serialVersionUID = 4949588177564901031L;
500
501 @Override
502 protected void populateItem(final ListItem<TopologyNode> item) {
503 item.add(topologyNodePanel("el", item.getModelObject(), false));
504 }
505 };
506 newlyCreated.setOutputMarkupId(true);
507 newlyCreated.setReuseItems(true);
508
509 newlyCreatedContainer.add(newlyCreated);
510 }
511
512 private static List<String> createConnections(final Map<Serializable, Map<Serializable, TopologyNode>> targets) {
513 List<String> list = new ArrayList<>();
514
515 targets.forEach((key, value) -> value.forEach((label, node) -> list.add(
516 String.format("connect('%s','%s','%s');", key, label, node.getKind()))));
517
518 return list;
519 }
520
521 private TopologyNodePanel topologyNodePanel(final String id, final TopologyNode node, final boolean errored) {
522 TopologyNodePanel panel = new TopologyNodePanel(id, node, errored);
523 panel.setMarkupId(String.valueOf(node.getKey()));
524 panel.setOutputMarkupId(true);
525
526 List<Behavior> behaviors = new ArrayList<>();
527
528 behaviors.add(new Behavior() {
529
530 private static final long serialVersionUID = 2661717818979056044L;
531
532 @Override
533 public void renderHead(final Component component, final IHeaderResponse response) {
534 response.render(OnDomReadyHeaderItem.forScript(String.format("setPosition('%s', %d, %d)",
535 node.getKey(), node.getX(), node.getY())));
536 }
537 });
538
539 behaviors.add(new AjaxEventBehavior(Constants.ON_CLICK) {
540
541 private static final long serialVersionUID = -9027652037484739586L;
542
543 @Override
544 protected String findIndicatorId() {
545 return StringUtils.EMPTY;
546 }
547
548 @Override
549 protected void onEvent(final AjaxRequestTarget target) {
550 togglePanel.toggleWithContent(target, node);
551 target.appendJavaScript(String.format(
552 "$('.window').removeClass(\"active-window\").addClass(\"inactive-window\"); "
553 + "$(document.getElementById('%s'))."
554 + "removeClass(\"inactive-window\").addClass(\"active-window\");", node.getKey()));
555 }
556 });
557
558 panel.add(behaviors.toArray(Behavior[]::new));
559
560 return panel;
561 }
562
563 @Override
564 @SuppressWarnings("unchecked")
565 public void onEvent(final IEvent<?> event) {
566 super.onEvent(event);
567
568 if (event.getPayload() instanceof CreateEvent) {
569 CreateEvent resourceCreateEvent = CreateEvent.class.cast(event.getPayload());
570
571 TopologyNode node = new TopologyNode(
572 resourceCreateEvent.getKey(),
573 resourceCreateEvent.getDisplayName(),
574 resourceCreateEvent.getKind());
575
576 newlyCreated.getModelObject().add(node);
577 resourceCreateEvent.getTarget().add(newlyCreatedContainer);
578
579 resourceCreateEvent.getTarget().appendJavaScript(String.format(
580 "window.Wicket.WebSocket.send('"
581 + "{\"kind\":\"%s\",\"target\":\"%s\",\"source\":\"%s\",\"scope\":\"%s\"}"
582 + "');",
583 SupportedOperation.ADD_ENDPOINT,
584 resourceCreateEvent.getKey(),
585 resourceCreateEvent.getParent(),
586 resourceCreateEvent.getKind()));
587 }
588 }
589 }