Merge "Add latency constraint"
diff --git a/apps/sdnip/src/main/java/org/onlab/onos/sdnip/bgp/BgpConstants.java b/apps/sdnip/src/main/java/org/onlab/onos/sdnip/bgp/BgpConstants.java
index 92f4f07..596720c 100644
--- a/apps/sdnip/src/main/java/org/onlab/onos/sdnip/bgp/BgpConstants.java
+++ b/apps/sdnip/src/main/java/org/onlab/onos/sdnip/bgp/BgpConstants.java
@@ -119,6 +119,31 @@
/** BGP UPDATE ORIGIN: INCOMPLETE. */
public static final int INCOMPLETE = 2;
+
+ /**
+ * Gets the BGP UPDATE origin type as a string.
+ *
+ * @param type the BGP UPDATE origin type
+ * @return the BGP UPDATE origin type as a string
+ */
+ public static String typeToString(int type) {
+ String typeString = "UNKNOWN";
+
+ switch (type) {
+ case IGP:
+ typeString = "IGP";
+ break;
+ case EGP:
+ typeString = "EGP";
+ break;
+ case INCOMPLETE:
+ typeString = "INCOMPLETE";
+ break;
+ default:
+ break;
+ }
+ return typeString;
+ }
}
/**
@@ -142,6 +167,28 @@
/** BGP UPDATE AS_PATH Type: AS_SEQUENCE. */
public static final int AS_SEQUENCE = 2;
+
+ /**
+ * Gets the BGP AS_PATH type as a string.
+ *
+ * @param type the BGP AS_PATH type
+ * @return the BGP AS_PATH type as a string
+ */
+ public static String typeToString(int type) {
+ String typeString = "UNKNOWN";
+
+ switch (type) {
+ case AS_SET:
+ typeString = "AS_SET";
+ break;
+ case AS_SEQUENCE:
+ typeString = "AS_SEQUENCE";
+ break;
+ default:
+ break;
+ }
+ return typeString;
+ }
}
/**
diff --git a/apps/sdnip/src/main/java/org/onlab/onos/sdnip/bgp/BgpRouteEntry.java b/apps/sdnip/src/main/java/org/onlab/onos/sdnip/bgp/BgpRouteEntry.java
index cd36f72..203e03c 100644
--- a/apps/sdnip/src/main/java/org/onlab/onos/sdnip/bgp/BgpRouteEntry.java
+++ b/apps/sdnip/src/main/java/org/onlab/onos/sdnip/bgp/BgpRouteEntry.java
@@ -309,7 +309,7 @@
@Override
public String toString() {
return MoreObjects.toStringHelper(getClass())
- .add("type", this.type)
+ .add("type", BgpConstants.Update.AsPath.typeToString(type))
.add("segmentAsNumbers", this.segmentAsNumbers)
.toString();
}
@@ -444,7 +444,7 @@
.add("prefix", prefix())
.add("nextHop", nextHop())
.add("bgpId", bgpSession.getRemoteBgpId())
- .add("origin", origin)
+ .add("origin", BgpConstants.Update.Origin.typeToString(origin))
.add("asPath", asPath)
.add("localPref", localPref)
.add("multiExitDisc", multiExitDisc)
diff --git a/apps/sdnip/src/main/java/org/onlab/onos/sdnip/cli/BgpRoutesListCommand.java b/apps/sdnip/src/main/java/org/onlab/onos/sdnip/cli/BgpRoutesListCommand.java
index 260cfe6..15cb554 100644
--- a/apps/sdnip/src/main/java/org/onlab/onos/sdnip/cli/BgpRoutesListCommand.java
+++ b/apps/sdnip/src/main/java/org/onlab/onos/sdnip/cli/BgpRoutesListCommand.java
@@ -15,10 +15,18 @@
*/
package org.onlab.onos.sdnip.cli;
+import java.util.Collection;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
import org.apache.karaf.shell.commands.Command;
+import org.apache.karaf.shell.commands.Option;
import org.onlab.onos.cli.AbstractShellCommand;
import org.onlab.onos.sdnip.SdnIpService;
-import org.onlab.onos.sdnip.bgp.BgpConstants;
+import org.onlab.onos.sdnip.bgp.BgpConstants.Update.AsPath;
+import org.onlab.onos.sdnip.bgp.BgpConstants.Update.Origin;
import org.onlab.onos.sdnip.bgp.BgpRouteEntry;
/**
@@ -27,46 +35,134 @@
@Command(scope = "onos", name = "bgp-routes",
description = "Lists all routes received from BGP")
public class BgpRoutesListCommand extends AbstractShellCommand {
+ @Option(name = "-s", aliases = "--summary",
+ description = "BGP routes summary",
+ required = false, multiValued = false)
+ private boolean routesSummary = false;
- private static final String FORMAT =
+ private static final String FORMAT_SUMMARY = "Total BGP routes = %d";
+ private static final String FORMAT_ROUTE =
"prefix=%s, nexthop=%s, origin=%s, localpref=%s, med=%s, aspath=%s, bgpid=%s";
@Override
protected void execute() {
SdnIpService service = get(SdnIpService.class);
- for (BgpRouteEntry route : service.getBgpRoutes()) {
- printRoute(route);
+ // Print summary of the routes
+ if (routesSummary) {
+ printSummary(service.getBgpRoutes());
+ return;
+ }
+
+ // Print all routes
+ printRoutes(service.getBgpRoutes());
+ }
+
+ /**
+ * Prints summary of the routes.
+ *
+ * @param routes the routes
+ */
+ private void printSummary(Collection<BgpRouteEntry> routes) {
+ if (outputJson()) {
+ ObjectMapper mapper = new ObjectMapper();
+ ObjectNode result = mapper.createObjectNode();
+ result.put("totalRoutes", routes.size());
+ print("%s", result);
+ } else {
+ print(FORMAT_SUMMARY, routes.size());
}
}
+ /**
+ * Prints all routes.
+ *
+ * @param routes the routes to print
+ */
+ private void printRoutes(Collection<BgpRouteEntry> routes) {
+ if (outputJson()) {
+ print("%s", json(routes));
+ } else {
+ for (BgpRouteEntry route : routes) {
+ printRoute(route);
+ }
+ }
+ }
+
+ /**
+ * Prints a BGP route.
+ *
+ * @param route the route to print
+ */
private void printRoute(BgpRouteEntry route) {
if (route != null) {
- print(FORMAT, route.prefix(), route.nextHop(),
- originToString(route.getOrigin()), route.getLocalPref(),
- route.getMultiExitDisc(), route.getAsPath(),
- route.getBgpSession().getRemoteBgpId());
+ print(FORMAT_ROUTE, route.prefix(), route.nextHop(),
+ Origin.typeToString(route.getOrigin()),
+ route.getLocalPref(), route.getMultiExitDisc(),
+ route.getAsPath(), route.getBgpSession().getRemoteBgpId());
}
}
- private static String originToString(int origin) {
- String originString = "UNKNOWN";
+ /**
+ * Produces a JSON array of routes.
+ *
+ * @param routes the routes with the data
+ * @return JSON array with the routes
+ */
+ private JsonNode json(Collection<BgpRouteEntry> routes) {
+ ObjectMapper mapper = new ObjectMapper();
+ ArrayNode result = mapper.createArrayNode();
- switch (origin) {
- case BgpConstants.Update.Origin.IGP:
- originString = "IGP";
- break;
- case BgpConstants.Update.Origin.EGP:
- originString = "EGP";
- break;
- case BgpConstants.Update.Origin.INCOMPLETE:
- originString = "INCOMPLETE";
- break;
- default:
- break;
+ for (BgpRouteEntry route : routes) {
+ result.add(json(mapper, route));
}
-
- return originString;
+ return result;
}
+ /**
+ * Produces JSON object for a route.
+ *
+ * @param mapper the JSON object mapper to use
+ * @param route the route with the data
+ * @return JSON object for the route
+ */
+ private ObjectNode json(ObjectMapper mapper, BgpRouteEntry route) {
+ ObjectNode result = mapper.createObjectNode();
+
+ result.put("prefix", route.prefix().toString());
+ result.put("nextHop", route.nextHop().toString());
+ result.put("bgpId", route.getBgpSession().getRemoteBgpId().toString());
+ result.put("origin", Origin.typeToString(route.getOrigin()));
+ result.put("asPath", json(mapper, route.getAsPath()));
+ result.put("localPref", route.getLocalPref());
+ result.put("multiExitDisc", route.getMultiExitDisc());
+
+ return result;
+ }
+
+ /**
+ * Produces JSON object for an AS path.
+ *
+ * @param mapper the JSON object mapper to use
+ * @param asPath the AS path with the data
+ * @return JSON object for the AS path
+ */
+ private ObjectNode json(ObjectMapper mapper, BgpRouteEntry.AsPath asPath) {
+ ObjectNode result = mapper.createObjectNode();
+ ArrayNode pathSegmentsJson = mapper.createArrayNode();
+ for (BgpRouteEntry.PathSegment pathSegment : asPath.getPathSegments()) {
+ ObjectNode pathSegmentJson = mapper.createObjectNode();
+ pathSegmentJson.put("type",
+ AsPath.typeToString(pathSegment.getType()));
+ ArrayNode segmentAsNumbersJson = mapper.createArrayNode();
+ for (Long asNumber : pathSegment.getSegmentAsNumbers()) {
+ segmentAsNumbersJson.add(asNumber);
+ }
+ pathSegmentJson.put("segmentAsNumbers", segmentAsNumbersJson);
+ pathSegmentsJson.add(pathSegmentJson);
+ }
+ result.put("pathSegments", pathSegmentsJson);
+
+ return result;
+ }
}
diff --git a/apps/sdnip/src/main/java/org/onlab/onos/sdnip/cli/RoutesListCommand.java b/apps/sdnip/src/main/java/org/onlab/onos/sdnip/cli/RoutesListCommand.java
index 0c44453..e9c25c4 100644
--- a/apps/sdnip/src/main/java/org/onlab/onos/sdnip/cli/RoutesListCommand.java
+++ b/apps/sdnip/src/main/java/org/onlab/onos/sdnip/cli/RoutesListCommand.java
@@ -15,7 +15,14 @@
*/
package org.onlab.onos.sdnip.cli;
+import java.util.Collection;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
import org.apache.karaf.shell.commands.Command;
+import org.apache.karaf.shell.commands.Option;
import org.onlab.onos.cli.AbstractShellCommand;
import org.onlab.onos.sdnip.RouteEntry;
import org.onlab.onos.sdnip.SdnIpService;
@@ -26,22 +33,100 @@
@Command(scope = "onos", name = "routes",
description = "Lists all routes known to SDN-IP")
public class RoutesListCommand extends AbstractShellCommand {
+ @Option(name = "-s", aliases = "--summary",
+ description = "SDN-IP routes summary",
+ required = false, multiValued = false)
+ private boolean routesSummary = false;
- private static final String FORMAT =
+ private static final String FORMAT_SUMMARY = "Total SDN-IP routes = %d";
+ private static final String FORMAT_ROUTE =
"prefix=%s, nexthop=%s";
@Override
protected void execute() {
SdnIpService service = get(SdnIpService.class);
- for (RouteEntry route : service.getRoutes()) {
- printRoute(route);
+ // Print summary of the routes
+ if (routesSummary) {
+ printSummary(service.getRoutes());
+ return;
+ }
+
+ // Print all routes
+ printRoutes(service.getRoutes());
+ }
+
+ /**
+ * Prints summary of the routes.
+ *
+ * @param routes the routes
+ */
+ private void printSummary(Collection<RouteEntry> routes) {
+ if (outputJson()) {
+ ObjectMapper mapper = new ObjectMapper();
+ ObjectNode result = mapper.createObjectNode();
+ result.put("totalRoutes", routes.size());
+ print("%s", result);
+ } else {
+ print(FORMAT_SUMMARY, routes.size());
}
}
+ /**
+ * Prints all routes.
+ *
+ * @param routes the routes to print
+ */
+ private void printRoutes(Collection<RouteEntry> routes) {
+ if (outputJson()) {
+ print("%s", json(routes));
+ } else {
+ for (RouteEntry route : routes) {
+ printRoute(route);
+ }
+ }
+ }
+
+ /**
+ * Prints a route.
+ *
+ * @param route the route to print
+ */
private void printRoute(RouteEntry route) {
if (route != null) {
- print(FORMAT, route.prefix(), route.nextHop());
+ print(FORMAT_ROUTE, route.prefix(), route.nextHop());
}
}
+
+ /**
+ * Produces a JSON array of routes.
+ *
+ * @param routes the routes with the data
+ * @return JSON array with the routes
+ */
+ private JsonNode json(Collection<RouteEntry> routes) {
+ ObjectMapper mapper = new ObjectMapper();
+ ArrayNode result = mapper.createArrayNode();
+
+ for (RouteEntry route : routes) {
+ result.add(json(mapper, route));
+ }
+ return result;
+ }
+
+ /**
+ * Produces JSON object for a route.
+ *
+ * @param mapper the JSON object mapper to use
+ * @param route the route with the data
+ * @return JSON object for the route
+ */
+ private ObjectNode json(ObjectMapper mapper, RouteEntry route) {
+ ObjectNode result = mapper.createObjectNode();
+
+ result.put("prefix", route.prefix().toString());
+ result.put("nextHop", route.nextHop().toString());
+
+ return result;
+ }
}
diff --git a/apps/sdnip/src/test/java/org/onlab/onos/sdnip/bgp/AsPathTest.java b/apps/sdnip/src/test/java/org/onlab/onos/sdnip/bgp/AsPathTest.java
index fd5a017..fddd895 100644
--- a/apps/sdnip/src/test/java/org/onlab/onos/sdnip/bgp/AsPathTest.java
+++ b/apps/sdnip/src/test/java/org/onlab/onos/sdnip/bgp/AsPathTest.java
@@ -66,8 +66,8 @@
String expectedString =
"AsPath{pathSegments=" +
- "[PathSegment{type=2, segmentAsNumbers=[1, 2, 3]}, " +
- "PathSegment{type=1, segmentAsNumbers=[4, 5, 6]}]}";
+ "[PathSegment{type=AS_SEQUENCE, segmentAsNumbers=[1, 2, 3]}, " +
+ "PathSegment{type=AS_SET, segmentAsNumbers=[4, 5, 6]}]}";
assertThat(asPath.toString(), is(expectedString));
}
@@ -177,8 +177,8 @@
String expectedString =
"AsPath{pathSegments=" +
- "[PathSegment{type=2, segmentAsNumbers=[1, 2, 3]}, " +
- "PathSegment{type=1, segmentAsNumbers=[4, 5, 6]}]}";
+ "[PathSegment{type=AS_SEQUENCE, segmentAsNumbers=[1, 2, 3]}, " +
+ "PathSegment{type=AS_SET, segmentAsNumbers=[4, 5, 6]}]}";
assertThat(asPath.toString(), is(expectedString));
}
}
diff --git a/apps/sdnip/src/test/java/org/onlab/onos/sdnip/bgp/BgpRouteEntryTest.java b/apps/sdnip/src/test/java/org/onlab/onos/sdnip/bgp/BgpRouteEntryTest.java
index d1ad3d4..318008f 100644
--- a/apps/sdnip/src/test/java/org/onlab/onos/sdnip/bgp/BgpRouteEntryTest.java
+++ b/apps/sdnip/src/test/java/org/onlab/onos/sdnip/bgp/BgpRouteEntryTest.java
@@ -130,9 +130,9 @@
String expectedString =
"BgpRouteEntry{prefix=1.2.3.0/24, nextHop=5.6.7.8, " +
- "bgpId=10.0.0.1, origin=0, asPath=AsPath{pathSegments=" +
- "[PathSegment{type=2, segmentAsNumbers=[1, 2, 3]}, " +
- "PathSegment{type=1, segmentAsNumbers=[4, 5, 6]}]}, " +
+ "bgpId=10.0.0.1, origin=IGP, asPath=AsPath{pathSegments=" +
+ "[PathSegment{type=AS_SEQUENCE, segmentAsNumbers=[1, 2, 3]}, " +
+ "PathSegment{type=AS_SET, segmentAsNumbers=[4, 5, 6]}]}, " +
"localPref=100, multiExitDisc=20}";
assertThat(bgpRouteEntry.toString(), is(expectedString));
}
@@ -504,9 +504,9 @@
String expectedString =
"BgpRouteEntry{prefix=1.2.3.0/24, nextHop=5.6.7.8, " +
- "bgpId=10.0.0.1, origin=0, asPath=AsPath{pathSegments=" +
- "[PathSegment{type=2, segmentAsNumbers=[1, 2, 3]}, " +
- "PathSegment{type=1, segmentAsNumbers=[4, 5, 6]}]}, " +
+ "bgpId=10.0.0.1, origin=IGP, asPath=AsPath{pathSegments=" +
+ "[PathSegment{type=AS_SEQUENCE, segmentAsNumbers=[1, 2, 3]}, " +
+ "PathSegment{type=AS_SET, segmentAsNumbers=[4, 5, 6]}]}, " +
"localPref=100, multiExitDisc=20}";
assertThat(bgpRouteEntry.toString(), is(expectedString));
}
diff --git a/apps/sdnip/src/test/java/org/onlab/onos/sdnip/bgp/PathSegmentTest.java b/apps/sdnip/src/test/java/org/onlab/onos/sdnip/bgp/PathSegmentTest.java
index f11d668..27579de 100644
--- a/apps/sdnip/src/test/java/org/onlab/onos/sdnip/bgp/PathSegmentTest.java
+++ b/apps/sdnip/src/test/java/org/onlab/onos/sdnip/bgp/PathSegmentTest.java
@@ -52,7 +52,7 @@
BgpRouteEntry.PathSegment pathSegment = generatePathSegment();
String expectedString =
- "PathSegment{type=2, segmentAsNumbers=[1, 2, 3]}";
+ "PathSegment{type=AS_SEQUENCE, segmentAsNumbers=[1, 2, 3]}";
assertThat(pathSegment.toString(), is(expectedString));
}
@@ -124,7 +124,7 @@
BgpRouteEntry.PathSegment pathSegment = generatePathSegment();
String expectedString =
- "PathSegment{type=2, segmentAsNumbers=[1, 2, 3]}";
+ "PathSegment{type=AS_SEQUENCE, segmentAsNumbers=[1, 2, 3]}";
assertThat(pathSegment.toString(), is(expectedString));
}
}
diff --git a/features/features.xml b/features/features.xml
index b8ef86f..efd31cf 100644
--- a/features/features.xml
+++ b/features/features.xml
@@ -119,6 +119,7 @@
description="ONOS GUI console components">
<feature>onos-api</feature>
<feature>onos-thirdparty-web</feature>
+ <bundle>mvn:org.eclipse.jetty/jetty-websocket/8.1.15.v20140411</bundle>
<bundle>mvn:org.onlab.onos/onos-gui/1.0.0-SNAPSHOT</bundle>
</feature>
diff --git a/web/gui/pom.xml b/web/gui/pom.xml
index 1b061a1..ab5b62c 100644
--- a/web/gui/pom.xml
+++ b/web/gui/pom.xml
@@ -35,4 +35,16 @@
<web.context>/onos/ui</web.context>
</properties>
+ <dependencies>
+ <dependency>
+ <groupId>org.eclipse.jetty</groupId>
+ <artifactId>jetty-websocket</artifactId>
+ <version>8.1.15.v20140411</version>
+ </dependency>
+ <dependency>
+ <groupId>javax.servlet</groupId>
+ <artifactId>servlet-api</artifactId>
+ <version>2.5</version>
+ </dependency>
+ </dependencies>
</project>
diff --git a/web/gui/src/main/java/org/onlab/onos/gui/GuiWebSocketServlet.java b/web/gui/src/main/java/org/onlab/onos/gui/GuiWebSocketServlet.java
new file mode 100644
index 0000000..02b46ed
--- /dev/null
+++ b/web/gui/src/main/java/org/onlab/onos/gui/GuiWebSocketServlet.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2014 Open Networking Laboratory
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.onlab.onos.gui;
+
+import org.eclipse.jetty.websocket.WebSocket;
+import org.eclipse.jetty.websocket.WebSocketServlet;
+import org.onlab.osgi.DefaultServiceDirectory;
+import org.onlab.osgi.ServiceDirectory;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * Web socket servlet capable of creating various sockets for the user interface.
+ */
+public class GuiWebSocketServlet extends WebSocketServlet {
+
+ private ServiceDirectory directory = new DefaultServiceDirectory();
+
+ @Override
+ public WebSocket doWebSocketConnect(HttpServletRequest request, String protocol) {
+
+ return new TopologyWebSocket(directory);
+ }
+
+}
diff --git a/web/gui/src/main/java/org/onlab/onos/gui/TopologyWebSocket.java b/web/gui/src/main/java/org/onlab/onos/gui/TopologyWebSocket.java
new file mode 100644
index 0000000..7828043
--- /dev/null
+++ b/web/gui/src/main/java/org/onlab/onos/gui/TopologyWebSocket.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2014 Open Networking Laboratory
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.onlab.onos.gui;
+
+import org.eclipse.jetty.websocket.WebSocket;
+import org.onlab.onos.net.device.DeviceService;
+import org.onlab.onos.net.topology.Topology;
+import org.onlab.onos.net.topology.TopologyEdge;
+import org.onlab.onos.net.topology.TopologyEvent;
+import org.onlab.onos.net.topology.TopologyGraph;
+import org.onlab.onos.net.topology.TopologyListener;
+import org.onlab.onos.net.topology.TopologyService;
+import org.onlab.onos.net.topology.TopologyVertex;
+import org.onlab.osgi.ServiceDirectory;
+
+import java.io.IOException;
+
+/**
+ * Web socket capable of interacting with the GUI topology view.
+ */
+public class TopologyWebSocket implements WebSocket.OnTextMessage, TopologyListener {
+
+ private final ServiceDirectory directory;
+ private final TopologyService topologyService;
+ private final DeviceService deviceService;
+
+ private Connection connection;
+
+ /**
+ * Creates a new web-socket for serving data to GUI topology view.
+ *
+ * @param directory service directory
+ */
+ public TopologyWebSocket(ServiceDirectory directory) {
+ this.directory = directory;
+ topologyService = directory.get(TopologyService.class);
+ deviceService = directory.get(DeviceService.class);
+ }
+
+ @Override
+ public void onOpen(Connection connection) {
+ this.connection = connection;
+
+ // Register for topology events...
+ if (topologyService != null && deviceService != null) {
+ topologyService.addListener(this);
+
+ sendMessage("Yo!!!");
+
+ Topology topology = topologyService.currentTopology();
+ TopologyGraph graph = topologyService.getGraph(topology);
+ for (TopologyVertex vertex : graph.getVertexes()) {
+ sendMessage(deviceService.getDevice(vertex.deviceId()).toString());
+ }
+
+ for (TopologyEdge edge : graph.getEdges()) {
+ sendMessage(edge.link().toString());
+ }
+
+ sendMessage("That's what we're starting with...");
+
+ } else {
+ sendMessage("No topology service!!!");
+ }
+ }
+
+ @Override
+ public void onClose(int closeCode, String message) {
+ TopologyService topologyService = directory.get(TopologyService.class);
+ if (topologyService != null) {
+ topologyService.removeListener(this);
+ }
+ }
+
+ @Override
+ public void onMessage(String data) {
+ System.out.println("Received: " + data);
+ }
+
+ public void sendMessage(String data) {
+ try {
+ connection.sendMessage(data);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ @Override
+ public void event(TopologyEvent event) {
+ sendMessage(event.toString());
+ }
+}
+
diff --git a/web/gui/src/main/webapp/WEB-INF/web.xml b/web/gui/src/main/webapp/WEB-INF/web.xml
index c5c6e28..ab7a550 100644
--- a/web/gui/src/main/webapp/WEB-INF/web.xml
+++ b/web/gui/src/main/webapp/WEB-INF/web.xml
@@ -39,4 +39,16 @@
<url-pattern>/rs/*</url-pattern>
</servlet-mapping>
+ <servlet>
+ <servlet-name>Web Socket Service</servlet-name>
+ <servlet-class>org.onlab.onos.gui.GuiWebSocketServlet</servlet-class>
+ <load-on-startup>2</load-on-startup>
+ </servlet>
+
+ <servlet-mapping>
+ <servlet-name>Web Socket Service</servlet-name>
+ <url-pattern>/ws/*</url-pattern>
+ </servlet-mapping>
+
+
</web-app>
\ No newline at end of file
diff --git a/web/gui/src/main/webapp/d3Utils.js b/web/gui/src/main/webapp/d3Utils.js
new file mode 100644
index 0000000..51651fa
--- /dev/null
+++ b/web/gui/src/main/webapp/d3Utils.js
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2014 Open Networking Laboratory
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ Utility functions for D3 visualizations.
+
+ @author Simon Hunt
+ */
+
+(function (onos) {
+ 'use strict';
+
+ function createDragBehavior(force, selectCb, atDragEnd) {
+ var draggedThreshold = d3.scale.linear()
+ .domain([0, 0.1])
+ .range([5, 20])
+ .clamp(true),
+ drag;
+
+ // TODO: better validation of parameters
+ if (!$.isFunction(selectCb)) {
+ alert('d3util.createDragBehavior(): selectCb is not a function')
+ }
+ if (!$.isFunction(atDragEnd)) {
+ alert('d3util.createDragBehavior(): atDragEnd is not a function')
+ }
+
+ function dragged(d) {
+ var threshold = draggedThreshold(force.alpha()),
+ dx = d.oldX - d.px,
+ dy = d.oldY - d.py;
+ if (Math.abs(dx) >= threshold || Math.abs(dy) >= threshold) {
+ d.dragged = true;
+ }
+ return d.dragged;
+ }
+
+ drag = d3.behavior.drag()
+ .origin(function(d) { return d; })
+ .on('dragstart', function(d) {
+ d.oldX = d.x;
+ d.oldY = d.y;
+ d.dragged = false;
+ d.fixed |= 2;
+ })
+ .on('drag', function(d) {
+ d.px = d3.event.x;
+ d.py = d3.event.y;
+ if (dragged(d)) {
+ if (!force.alpha()) {
+ force.alpha(.025);
+ }
+ }
+ })
+ .on('dragend', function(d) {
+ if (!dragged(d)) {
+ // consider this the same as a 'click' (selection of node)
+ selectCb(d, this); // TODO: set 'this' context instead of param
+ }
+ d.fixed &= ~6;
+
+ // hook at the end of a drag gesture
+ atDragEnd(d, this); // TODO: set 'this' context instead of param
+ });
+
+ return drag;
+ }
+
+ function appendGlow(svg) {
+ // TODO: parameterize color
+
+ var glow = svg.append('filter')
+ .attr('x', '-50%')
+ .attr('y', '-50%')
+ .attr('width', '200%')
+ .attr('height', '200%')
+ .attr('id', 'blue-glow');
+
+ glow.append('feColorMatrix')
+ .attr('type', 'matrix')
+ .attr('values', '0 0 0 0 0 ' +
+ '0 0 0 0 0 ' +
+ '0 0 0 0 .7 ' +
+ '0 0 0 1 0 ');
+
+ glow.append('feGaussianBlur')
+ .attr('stdDeviation', 3)
+ .attr('result', 'coloredBlur');
+
+ glow.append('feMerge').selectAll('feMergeNode')
+ .data(['coloredBlur', 'SourceGraphic'])
+ .enter().append('feMergeNode')
+ .attr('in', String);
+ }
+
+ // === register the functions as a library
+ onos.ui.addLib('d3util', {
+ createDragBehavior: createDragBehavior,
+ appendGlow: appendGlow
+ });
+
+}(ONOS));
diff --git a/web/gui/src/main/webapp/index2.html b/web/gui/src/main/webapp/index2.html
index 1ddc318..235c1d7 100644
--- a/web/gui/src/main/webapp/index2.html
+++ b/web/gui/src/main/webapp/index2.html
@@ -64,6 +64,9 @@
<div id="overlays">
<!-- NOTE: overlays injected here, as needed -->
</div>
+ <div id="alerts">
+ <!-- NOTE: alert content injected here, as needed -->
+ </div>
</div>
<!-- Initialize the UI...-->
@@ -76,6 +79,9 @@
});
</script>
+ <!-- Library module files included here -->
+ <script src="d3Utils.js"></script>
+
<!-- Framework module files included here -->
<script src="mast2.js"></script>
diff --git a/web/gui/src/main/webapp/json/eventTest_11.json b/web/gui/src/main/webapp/json/eventTest_11.json
index e907444..3b361d5 100644
--- a/web/gui/src/main/webapp/json/eventTest_11.json
+++ b/web/gui/src/main/webapp/json/eventTest_11.json
@@ -10,8 +10,8 @@
"?"
],
"metaUi": {
- "x": 832,
- "y": 223
+ "Zx": 832,
+ "Zy": 223
}
}
}
diff --git a/web/gui/src/main/webapp/json/eventTest_15.json b/web/gui/src/main/webapp/json/eventTest_15.json
index 3622122..30ba9f3 100644
--- a/web/gui/src/main/webapp/json/eventTest_15.json
+++ b/web/gui/src/main/webapp/json/eventTest_15.json
@@ -10,8 +10,8 @@
"?"
],
"metaUi": {
- "x": 840,
- "y": 290
+ "Zx": 840,
+ "Zy": 290
}
}
}
diff --git a/web/gui/src/main/webapp/json/eventTest_17.json b/web/gui/src/main/webapp/json/eventTest_17.json
index e217456..82272a4 100644
--- a/web/gui/src/main/webapp/json/eventTest_17.json
+++ b/web/gui/src/main/webapp/json/eventTest_17.json
@@ -6,7 +6,7 @@
"dst": "of:0000ffffffffff05",
"dstPort": "10",
"type": "optical",
- "linkWidth": 2,
+ "linkWidth": 6,
"props" : {
"BW": "80 G"
}
diff --git a/web/gui/src/main/webapp/json/eventTest_23.json b/web/gui/src/main/webapp/json/eventTest_23.json
index 54383bc..fff0f2b 100644
--- a/web/gui/src/main/webapp/json/eventTest_23.json
+++ b/web/gui/src/main/webapp/json/eventTest_23.json
@@ -6,7 +6,7 @@
"dst": "of:0000ffffffffff05",
"dstPort": "30",
"type": "optical",
- "linkWidth": 2,
+ "linkWidth": 6,
"props" : {
"BW": "70 G"
}
diff --git a/web/gui/src/main/webapp/json/eventTest_24.json b/web/gui/src/main/webapp/json/eventTest_24.json
index 2287f6c..756b6c1 100644
--- a/web/gui/src/main/webapp/json/eventTest_24.json
+++ b/web/gui/src/main/webapp/json/eventTest_24.json
@@ -6,7 +6,7 @@
"dst": "of:0000ffffffffff08",
"dstPort": "20",
"type": "optical",
- "linkWidth": 2,
+ "linkWidth": 6,
"props" : {
"BW": "70 G"
}
diff --git a/web/gui/src/main/webapp/json/eventTest_30.json b/web/gui/src/main/webapp/json/eventTest_30.json
index ae2d4c1..a617f45 100644
--- a/web/gui/src/main/webapp/json/eventTest_30.json
+++ b/web/gui/src/main/webapp/json/eventTest_30.json
@@ -6,7 +6,7 @@
"dst": "of:0000ffffffffff08",
"dstPort": "30",
"type": "optical",
- "linkWidth": 2,
+ "linkWidth": 6,
"props" : {
"BW": "70 G"
}
diff --git a/web/gui/src/main/webapp/json/eventTest_34.json b/web/gui/src/main/webapp/json/eventTest_34.json
index 96015b5..fa5e3bc 100644
--- a/web/gui/src/main/webapp/json/eventTest_34.json
+++ b/web/gui/src/main/webapp/json/eventTest_34.json
@@ -6,7 +6,7 @@
"dst": "of:0000ffffffffff08",
"dstPort": "10",
"type": "optical",
- "linkWidth": 2,
+ "linkWidth": 6,
"props" : {
"BW": "70 G"
}
diff --git a/web/gui/src/main/webapp/json/eventTest_35.json b/web/gui/src/main/webapp/json/eventTest_35.json
new file mode 100644
index 0000000..c579e59
--- /dev/null
+++ b/web/gui/src/main/webapp/json/eventTest_35.json
@@ -0,0 +1,14 @@
+{
+ "event": "addLink",
+ "payload": {
+ "src": "of:0000ffffffffff04",
+ "srcPort": "27",
+ "dst": "of:0000ffffffffff08",
+ "dstPort": "10",
+ "type": "optical",
+ "linkWidth": 2,
+ "props" : {
+ "BW": "30 G"
+ }
+ }
+}
diff --git a/web/gui/src/main/webapp/onos2.css b/web/gui/src/main/webapp/onos2.css
index 983f288..748cc97 100644
--- a/web/gui/src/main/webapp/onos2.css
+++ b/web/gui/src/main/webapp/onos2.css
@@ -32,6 +32,34 @@
display: block;
}
+div#alerts {
+ display: none;
+ position: absolute;
+ z-index: 2000;
+ opacity: 0.65;
+ background-color: #006;
+ color: white;
+ top: 80px;
+ left: 40px;
+ padding: 3px 6px;
+ box-shadow: 4px 6px 12px #777;
+}
+
+div#alerts pre {
+ margin: 0.2em 6px;
+}
+
+div#alerts span.close {
+ color: #6af;
+ float: right;
+ right: 2px;
+ cursor: pointer;
+}
+
+div#alerts span.close:hover {
+ color: #fff;
+}
+
/*
* ==============================================================
* END OF NEW ONOS.JS file
@@ -54,12 +82,6 @@
* Network Graph elements ======================================
*/
-svg .link {
- opacity: .7;
-}
-
-svg .link.host {
-}
svg g.portLayer rect.port {
fill: #ccc;
@@ -70,33 +92,13 @@
pointer-events: none;
}
-svg .node.device rect {
- stroke-width: 1.5px;
-}
-svg .node.device.fixed rect {
- stroke-width: 1.5;
- stroke: #ccc;
-}
-
-svg .node.device.roadm rect {
- fill: #03c;
-}
-
-svg .node.device.switch rect {
- fill: #06f;
-}
svg .node.host circle {
fill: #c96;
stroke: #000;
}
-svg .node text {
- fill: white;
- font: 10pt sans-serif;
- pointer-events: none;
-}
/* for debugging */
svg .node circle.debug {
@@ -110,10 +112,6 @@
}
-svg .node.selected rect,
-svg .node.selected circle {
- filter: url(#blue-glow);
-}
svg .link.inactive,
svg .port.inactive,
diff --git a/web/gui/src/main/webapp/onos2.js b/web/gui/src/main/webapp/onos2.js
index 375fe6b..31d89fa 100644
--- a/web/gui/src/main/webapp/onos2.js
+++ b/web/gui/src/main/webapp/onos2.js
@@ -32,7 +32,8 @@
$.onos = function (options) {
var uiApi,
viewApi,
- navApi;
+ navApi,
+ libApi;
var defaultOptions = {
trace: false,
@@ -331,6 +332,58 @@
}
}
+ var alerts = {
+ open: false,
+ count: 0
+ };
+
+ function createAlerts() {
+ var al = d3.select('#alerts')
+ .style('display', 'block');
+ al.append('span')
+ .attr('class', 'close')
+ .text('X')
+ .on('click', closeAlerts);
+ al.append('pre');
+ alerts.open = true;
+ alerts.count = 0;
+ }
+
+ function closeAlerts() {
+ d3.select('#alerts')
+ .style('display', 'none');
+ d3.select('#alerts span').remove();
+ d3.select('#alerts pre').remove();
+ alerts.open = false;
+ }
+
+ function addAlert(msg) {
+ var lines,
+ oldContent;
+
+ if (alerts.count) {
+ oldContent = d3.select('#alerts pre').html();
+ }
+
+ lines = msg.split('\n');
+ lines[0] += ' '; // spacing so we don't crowd 'X'
+ lines = lines.join('\n');
+
+ if (oldContent) {
+ lines += '\n----\n' + oldContent;
+ }
+
+ d3.select('#alerts pre').html(lines);
+ alerts.count++;
+ }
+
+ function doAlert(msg) {
+ if (!alerts.open) {
+ createAlerts();
+ }
+ addAlert(msg);
+ }
+
function keyIn() {
var event = d3.event,
keyCode = event.keyCode,
@@ -408,7 +461,8 @@
uid: this.uid,
setRadio: this.setRadio,
setKeys: this.setKeys,
- dataLoadError: this.dataLoadError
+ dataLoadError: this.dataLoadError,
+ alert: this.alert
}
},
@@ -501,14 +555,20 @@
return makeUid(this, id);
},
- // TODO : implement custom dialogs (don't use alerts)
+ // TODO : implement custom dialogs
+
+ // Consider enhancing alert mechanism to handle multiples
+ // as individually closable.
+ alert: function (msg) {
+ doAlert(msg);
+ },
dataLoadError: function (err, url) {
var msg = 'Data Load Error\n\n' +
err.status + ' -- ' + err.statusText + '\n\n' +
'relative-url: "' + url + '"\n\n' +
'complete-url: "' + err.responseURL + '"';
- alert(msg);
+ this.alert(msg);
}
// TODO: consider schedule, clearTimer, etc.
@@ -521,6 +581,12 @@
// UI API
uiApi = {
+ addLib: function (libName, api) {
+ // TODO: validation of args
+ libApi[libName] = api;
+ },
+
+ // TODO: it remains to be seen whether we keep this style of docs
/** @api ui addView( vid, nid, cb )
* Adds a view to the UI.
* <p>
@@ -590,6 +656,12 @@
};
// ..........................................................
+ // Library API
+ libApi = {
+
+ };
+
+ // ..........................................................
// Exported API
// function to be called from index.html to build the ONOS UI
@@ -623,7 +695,8 @@
// export the api and build-UI function
return {
ui: uiApi,
- view: viewApi,
+ lib: libApi,
+ //view: viewApi,
nav: navApi,
buildUi: buildOnosUi
};
diff --git a/web/gui/src/main/webapp/topo2.css b/web/gui/src/main/webapp/topo2.css
index 8293ce4..c4d9a2d 100644
--- a/web/gui/src/main/webapp/topo2.css
+++ b/web/gui/src/main/webapp/topo2.css
@@ -24,12 +24,18 @@
opacity: 0.5;
}
+/* NODES */
+
svg .node.device {
stroke: none;
stroke-width: 1.5px;
cursor: pointer;
}
+svg .node.device rect {
+ stroke-width: 1.5px;
+}
+
svg .node.device.fixed rect {
stroke-width: 1.5;
stroke: #ccc;
@@ -50,6 +56,17 @@
pointer-events: none;
}
+svg .node.selected rect,
+svg .node.selected circle {
+ filter: url(#blue-glow);
+}
+
+/* LINKS */
+
+svg .link {
+ opacity: .7;
+}
+
/* for debugging */
svg .node circle.debug {
fill: white;
diff --git a/web/gui/src/main/webapp/topo2.js b/web/gui/src/main/webapp/topo2.js
index bd5a577..e7444c9 100644
--- a/web/gui/src/main/webapp/topo2.js
+++ b/web/gui/src/main/webapp/topo2.js
@@ -23,6 +23,9 @@
(function (onos) {
'use strict';
+ // shorter names for library APIs
+ var d3u = onos.lib.d3util;
+
// configuration data
var config = {
useLiveData: false,
@@ -37,6 +40,7 @@
showBackground: true
},
backgroundUrl: 'img/us-map.png',
+ webSockUrl: 'ws/topology',
data: {
live: {
jsonUrl: 'rs/topology/graph',
@@ -61,6 +65,10 @@
height: 14
}
},
+ topo: {
+ linkInColor: '#66f',
+ linkInWidth: 14
+ },
icons: {
w: 28,
h: 28,
@@ -106,7 +114,9 @@
// key bindings
var keyDispatch = {
space: injectTestEvent, // TODO: remove (testing only)
- // M: testMe, // TODO: remove (testing only)
+ S: injectStartupEvents, // TODO: remove (testing only)
+ A: testAlert, // TODO: remove (testing only)
+ M: testMe, // TODO: remove (testing only)
B: toggleBg,
G: toggleLayout,
@@ -121,6 +131,7 @@
links: [],
lookup: {}
},
+ webSock,
labelIdx = 0,
selected = {},
highlighted = null,
@@ -141,7 +152,8 @@
// For Debugging / Development
var eventPrefix = 'json/eventTest_',
- eventNumber = 0;
+ eventNumber = 0,
+ alertNumber = 0;
function note(label, msg) {
console.log('NOTE: ' + label + ': ' + msg);
@@ -155,22 +167,12 @@
// ==============================
// Key Callbacks
+ function testAlert(view) {
+ alertNumber++;
+ view.alert("Test me! -- " + alertNumber);
+ }
+
function testMe(view) {
- svg.append('line')
- .attr({
- x1: 100,
- y1: 100,
- x2: 500,
- y2: 400,
- stroke: '#2f3',
- 'stroke-width': 8
- })
- .transition()
- .duration(1200)
- .attr({
- stroke: '#666',
- 'stroke-width': 6
- });
}
function injectTestEvent(view) {
@@ -187,6 +189,13 @@
});
}
+ function injectStartupEvents(view) {
+ var lastStartupEvent = 32;
+ while (eventNumber < lastStartupEvent) {
+ injectTestEvent(view);
+ }
+ }
+
function toggleBg() {
var vis = bgImg.style('visibility');
bgImg.style('visibility', (vis === 'hidden') ? 'visible' : 'hidden');
@@ -370,6 +379,11 @@
return lnk;
}
+ function linkWidth(w) {
+ // w is number of links between nodes. Scale appropriately.
+ return w * 1.2;
+ }
+
function updateLinks() {
link = linkG.selectAll('.link')
.data(network.links, function (d) { return d.id; });
@@ -387,12 +401,12 @@
y1: function (d) { return d.y1; },
x2: function (d) { return d.x2; },
y2: function (d) { return d.y2; },
- stroke: '#66f',
- 'stroke-width': 10
+ stroke: config.topo.linkInColor,
+ 'stroke-width': config.topo.linkInWidth
})
.transition().duration(1000)
.attr({
- 'stroke-width': function (d) { return d.width; },
+ 'stroke-width': function (d) { return linkWidth(d.width); },
stroke: '#666' // TODO: remove explicit stroke, rather...
});
@@ -461,6 +475,10 @@
return box;
}
+ function mkSvgClass(d) {
+ return d.fixed ? d.svgClass + ' fixed' : d.svgClass;
+ }
+
function updateNodes() {
node = nodeG.selectAll('.node')
.data(network.nodes, function (d) { return d.id; });
@@ -473,11 +491,11 @@
.append('g')
.attr({
id: function (d) { return safeId(d.id); },
- class: function (d) { return d.svgClass; },
+ class: mkSvgClass,
transform: function (d) { return translate(d.x, d.y); },
opacity: 0
})
- //.call(network.drag)
+ .call(network.drag)
//.on('mouseover', function (d) {})
//.on('mouseover', function (d) {})
.transition()
@@ -563,6 +581,52 @@
}
// ==============================
+ // Web-Socket for live data
+
+ function webSockUrl() {
+ return document.location.toString()
+ .replace(/\#.*/, '')
+ .replace('http://', 'ws://')
+ .replace('https://', 'wss://')
+ .replace('index2.html', config.webSockUrl);
+ }
+
+ webSock = {
+ ws : null,
+
+ connect : function() {
+ webSock.ws = new WebSocket(webSockUrl());
+
+ webSock.ws.onopen = function() {
+ webSock._send("Hi there!");
+ };
+
+ webSock.ws.onmessage = function(m) {
+ if (m.data) {
+ console.log(m.data);
+ }
+ };
+
+ webSock.ws.onclose = function(m) {
+ webSock.ws = null;
+ };
+ },
+
+ send : function(text) {
+ if (text != null && text.length > 0) {
+ webSock._send(text);
+ }
+ },
+
+ _send : function(message) {
+ if (webSock.ws) {
+ webSock.ws.send(message);
+ }
+ }
+
+ };
+
+ // ==============================
// View life-cycle callbacks
function preload(view, ctx) {
@@ -578,6 +642,9 @@
svg = view.$div.append('svg');
setSize(svg, view);
+ // add blue glow filter to svg layer
+ d3u.appendGlow(svg);
+
// load the background image
bgImg = svg.append('svg:image')
.attr({
@@ -612,6 +679,20 @@
return fcfg.charge[d.class] || -200;
}
+ function selectCb(d, self) {
+ // TODO: selectObject(d, self);
+ }
+
+ function atDragEnd(d, self) {
+ // once we've finished moving, pin the node in position,
+ // if it is a device (not a host)
+ if (d.class === 'device') {
+ d.fixed = true;
+ d3.select(self).classed('fixed', true)
+ // TODO: send new [x,y] back to server, via websocket.
+ }
+ }
+
// set up the force layout
network.force = d3.layout.force()
.size(forceDim)
@@ -621,8 +702,10 @@
.linkDistance(ldist)
.linkStrength(lstrg)
.on('tick', tick);
- }
+ network.drag = d3u.createDragBehavior(network.force, selectCb, atDragEnd);
+ webSock.connect();
+ }
function load(view, ctx) {
// cache the view token, so network topo functions can access it
diff --git a/web/gui/src/main/webapp/ws.html b/web/gui/src/main/webapp/ws.html
new file mode 100644
index 0000000..cac99b2
--- /dev/null
+++ b/web/gui/src/main/webapp/ws.html
@@ -0,0 +1,54 @@
+<html>
+<head>
+ <title>Web Sockets Demo</title>
+
+ <script src="libs/jquery-2.1.1.min.js"></script>
+
+ <script type='text/javascript'>
+ if (!window.WebSocket)
+ alert("WebSocket not supported by this browser");
+
+ var server = {
+ connect : function() {
+ var location = document.location.toString().replace('http://',
+ 'ws://').replace('https://', 'wss://').replace('ws.html','ws/topology');
+ this.ws = new WebSocket(location);
+
+ this.ws.onopen = function() {
+ server._send("Hi there!");
+ };
+
+ this.ws.onmessage = function(m) {
+ if (m.data) {
+ $('#log').append(m.data).append($('<br/>'));
+ }
+ };
+
+ this.ws.onclose = function(m) {
+ this.ws = null;
+ };
+ },
+
+ _send : function(message) {
+ if (this.ws) {
+ this.ws.send(message);
+ }
+ },
+
+ send : function(text) {
+ if (text != null && text.length > 0) {
+ server._send(text);
+ }
+ }
+ };
+ </script>
+</head>
+<body>
+<pre id='log'></pre>
+
+<script type='text/javascript'>
+ server.connect();
+</script>
+
+</body>
+</html>
diff --git a/web/pom.xml b/web/pom.xml
index f846078..aad2134 100644
--- a/web/pom.xml
+++ b/web/pom.xml
@@ -131,6 +131,7 @@
com.fasterxml.jackson.databind,
com.fasterxml.jackson.databind.node,
com.google.common.base.*,
+ org.eclipse.jetty.websocket.*,
org.onlab.api.*,
org.onlab.osgi.*,
org.onlab.packet.*,