View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
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         // Add Zoom panel
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         // Add Syncope (root topologynode)
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         // required to retrieve parent positions
214         Map<String, TopologyNode> servers = new HashMap<>();
215         Map<String, TopologyNode> connectors = new HashMap<>();
216         // -----------------------------------------
217 
218         // -----------------------------------------
219         // Add Connector Servers
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         // Add File Paths
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         // Add Connector Intances
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()) // [SYNCOPE-1233]
319                                 ? conn.getBundleName() : conn.getDisplayName(),
320                                 TopologyNode.Kind.CONNECTOR);
321 
322                         // Define the parent note
323                         TopologyNode parent = servers.get(conn.getLocation());
324 
325                         // Set the position
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                         // Update connections
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         // Add Resources
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                         // Set position
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         // Create connections
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 }