slowr | db071b2 | 2017-07-07 11:10:25 -0700 | [diff] [blame] | 1 | /* |
Dimitrios Mavrommatis | f0c0632 | 2017-10-31 23:49:04 -0700 | [diff] [blame] | 2 | * Copyright 2017-present Open Networking Foundation |
slowr | db071b2 | 2017-07-07 11:10:25 -0700 | [diff] [blame] | 3 | * |
| 4 | * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | * you may not use this file except in compliance with the License. |
| 6 | * You may obtain a copy of the License at |
| 7 | * |
| 8 | * http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | * |
| 10 | * Unless required by applicable law or agreed to in writing, software |
| 11 | * distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | * See the License for the specific language governing permissions and |
| 14 | * limitations under the License. |
| 15 | */ |
| 16 | package org.onosproject.artemis.impl; |
| 17 | |
| 18 | import com.fasterxml.jackson.databind.JsonNode; |
| 19 | import com.google.common.collect.Maps; |
| 20 | import com.google.common.collect.Sets; |
Dimitrios Mavrommatis | f0c0632 | 2017-10-31 23:49:04 -0700 | [diff] [blame] | 21 | import com.google.common.collect.Streams; |
slowr | db071b2 | 2017-07-07 11:10:25 -0700 | [diff] [blame] | 22 | import org.json.JSONArray; |
| 23 | import org.json.JSONException; |
Dimitrios Mavrommatis | f0c0632 | 2017-10-31 23:49:04 -0700 | [diff] [blame] | 24 | import org.onlab.packet.IpAddress; |
slowr | db071b2 | 2017-07-07 11:10:25 -0700 | [diff] [blame] | 25 | import org.onlab.packet.IpPrefix; |
| 26 | import org.onosproject.core.ApplicationId; |
| 27 | import org.onosproject.net.config.Config; |
| 28 | import org.slf4j.Logger; |
| 29 | import org.slf4j.LoggerFactory; |
| 30 | |
| 31 | import java.util.ArrayList; |
| 32 | import java.util.Collections; |
| 33 | import java.util.Map; |
| 34 | import java.util.Objects; |
| 35 | import java.util.Set; |
Dimitrios Mavrommatis | f0c0632 | 2017-10-31 23:49:04 -0700 | [diff] [blame] | 36 | import java.util.stream.Collectors; |
slowr | db071b2 | 2017-07-07 11:10:25 -0700 | [diff] [blame] | 37 | |
| 38 | import static com.google.common.base.Preconditions.checkNotNull; |
| 39 | |
| 40 | /** |
| 41 | * Artemis Configuration Class. |
| 42 | */ |
Dimitrios Mavrommatis | f0c0632 | 2017-10-31 23:49:04 -0700 | [diff] [blame] | 43 | public class ArtemisConfig extends Config<ApplicationId> { |
slowr | db071b2 | 2017-07-07 11:10:25 -0700 | [diff] [blame] | 44 | private static final String PREFIXES = "prefixes"; |
| 45 | /* */ |
| 46 | private static final String PREFIX = "prefix"; |
| 47 | private static final String PATHS = "paths"; |
| 48 | private static final String MOAS = "moas"; |
| 49 | /* */ |
| 50 | private static final String ORIGIN = "origin"; |
| 51 | private static final String NEIGHBOR = "neighbor"; |
| 52 | private static final String ASN = "asn"; |
| 53 | /* */ |
slowr | db071b2 | 2017-07-07 11:10:25 -0700 | [diff] [blame] | 54 | private static final String MONITORS = "monitors"; |
| 55 | /* */ |
| 56 | private static final String RIPE = "ripe"; |
| 57 | private static final String EXABGP = "exabgp"; |
| 58 | /* */ |
Dimitrios Mavrommatis | f0c0632 | 2017-10-31 23:49:04 -0700 | [diff] [blame] | 59 | private static final String MOAS_LEGIT = "legit"; |
| 60 | private static final String TUNNEL_POINTS = "tunnelPoints"; |
| 61 | private static final String TUNNEL_OVSDB_IP = "ovsdb_ip"; |
| 62 | private static final String TUNNEL_LOCAL_IP = "local_ip"; |
| 63 | private static final String TUNNEL_OVS_PORT = "ovs_port"; |
slowr | db071b2 | 2017-07-07 11:10:25 -0700 | [diff] [blame] | 64 | |
| 65 | private final Logger log = LoggerFactory.getLogger(getClass()); |
| 66 | |
Dimitrios Mavrommatis | f0c0632 | 2017-10-31 23:49:04 -0700 | [diff] [blame] | 67 | Set<IpPrefix> prefixesToMonitor() { |
| 68 | JsonNode prefixesNode = object.path(PREFIXES); |
| 69 | if (!prefixesNode.isMissingNode()) { |
| 70 | return Streams.stream(prefixesNode) |
| 71 | .map(prefix -> IpPrefix.valueOf(prefix.get(PREFIX).asText())) |
| 72 | .collect(Collectors.toSet()); |
| 73 | } |
| 74 | return null; |
| 75 | } |
| 76 | |
slowr | db071b2 | 2017-07-07 11:10:25 -0700 | [diff] [blame] | 77 | /** |
| 78 | * Gets the set of monitored prefixes with the details (prefix, paths and MOAS). |
| 79 | * |
| 80 | * @return artemis class prefixes |
| 81 | */ |
| 82 | Set<ArtemisPrefixes> monitoredPrefixes() { |
| 83 | Set<ArtemisPrefixes> prefixes = Sets.newHashSet(); |
| 84 | |
Dimitrios Mavrommatis | f0c0632 | 2017-10-31 23:49:04 -0700 | [diff] [blame] | 85 | JsonNode prefixesNode = object.path(PREFIXES); |
| 86 | if (prefixesNode.isMissingNode()) { |
slowr | db071b2 | 2017-07-07 11:10:25 -0700 | [diff] [blame] | 87 | log.warn("prefixes field is null!"); |
| 88 | return prefixes; |
| 89 | } |
| 90 | |
| 91 | prefixesNode.forEach(jsonNode -> { |
| 92 | IpPrefix prefix = IpPrefix.valueOf(jsonNode.get(PREFIX).asText()); |
| 93 | |
slowr | db071b2 | 2017-07-07 11:10:25 -0700 | [diff] [blame] | 94 | JsonNode moasNode = jsonNode.get(MOAS); |
Dimitrios Mavrommatis | f0c0632 | 2017-10-31 23:49:04 -0700 | [diff] [blame] | 95 | Set<IpAddress> moasIps = Streams.stream(moasNode) |
| 96 | .map(asn -> IpAddress.valueOf(asn.asText())) |
| 97 | .collect(Collectors.toSet()); |
slowr | db071b2 | 2017-07-07 11:10:25 -0700 | [diff] [blame] | 98 | |
slowr | db071b2 | 2017-07-07 11:10:25 -0700 | [diff] [blame] | 99 | JsonNode pathsNode = jsonNode.get(PATHS); |
Dimitrios Mavrommatis | f0c0632 | 2017-10-31 23:49:04 -0700 | [diff] [blame] | 100 | Map<Integer, Map<Integer, Set<Integer>>> paths = Maps.newHashMap(); |
| 101 | pathsNode.forEach(path -> addPath(paths, path)); |
slowr | db071b2 | 2017-07-07 11:10:25 -0700 | [diff] [blame] | 102 | |
Dimitrios Mavrommatis | f0c0632 | 2017-10-31 23:49:04 -0700 | [diff] [blame] | 103 | prefixes.add(new ArtemisPrefixes(prefix, moasIps, paths)); |
slowr | db071b2 | 2017-07-07 11:10:25 -0700 | [diff] [blame] | 104 | }); |
| 105 | |
| 106 | return prefixes; |
| 107 | } |
| 108 | |
| 109 | /** |
| 110 | * Appends an ASN path on the ASN paths list of the Artemis application. |
| 111 | * |
| 112 | * @param paths active ASN paths list |
| 113 | * @param path ASN path to be added |
| 114 | */ |
| 115 | private void addPath(Map<Integer, Map<Integer, Set<Integer>>> paths, JsonNode path) { |
| 116 | Integer origin = path.path(ORIGIN).asInt(); |
| 117 | |
| 118 | JsonNode firstNeighborNode = path.path(NEIGHBOR); |
| 119 | // Check if neighbor exists in the configuration |
| 120 | if (!firstNeighborNode.isMissingNode()) { |
| 121 | firstNeighborNode.forEach(firstNeighbor -> { |
| 122 | Integer firstNeighborAsn = firstNeighbor.get(ASN).asInt(); |
| 123 | |
| 124 | JsonNode secondNeighborNode = firstNeighbor.path(NEIGHBOR); |
| 125 | // check if second neighbor exists in configuration |
| 126 | if (!secondNeighborNode.isMissingNode()) { |
| 127 | secondNeighborNode.forEach(secondNeighbor -> { |
| 128 | Integer secondNeighborAsn = secondNeighbor.asInt(); |
| 129 | |
| 130 | if (paths.containsKey(origin)) { |
| 131 | // paths already contain origin ASN. |
| 132 | Map<Integer, Set<Integer>> integerSetMap = paths.get(origin); |
| 133 | if (integerSetMap.containsKey(firstNeighborAsn)) { |
| 134 | integerSetMap.get(firstNeighborAsn).add(secondNeighborAsn); |
| 135 | } else { |
| 136 | paths.get(origin).put(firstNeighborAsn, Sets.newHashSet(secondNeighborAsn)); |
| 137 | } |
| 138 | } else { |
| 139 | // origin ASN does not exist in Map. |
| 140 | Map<Integer, Set<Integer>> first2second = Maps.newHashMap(); |
| 141 | first2second.put(firstNeighborAsn, Sets.newHashSet(secondNeighborAsn)); |
| 142 | paths.put(origin, first2second); |
| 143 | } |
| 144 | }); |
Dimitrios Mavrommatis | f0c0632 | 2017-10-31 23:49:04 -0700 | [diff] [blame] | 145 | // else append to paths without second neighbor |
slowr | db071b2 | 2017-07-07 11:10:25 -0700 | [diff] [blame] | 146 | } else { |
| 147 | if (!paths.containsKey(origin)) { |
| 148 | Map<Integer, Set<Integer>> first2second = Maps.newHashMap(); |
| 149 | first2second.put(firstNeighborAsn, Sets.newHashSet()); |
| 150 | paths.put(origin, first2second); |
| 151 | } else { |
| 152 | // paths already contain origin ASN. |
| 153 | Map<Integer, Set<Integer>> integerSetMap = paths.get(origin); |
| 154 | if (!integerSetMap.containsKey(firstNeighborAsn)) { |
| 155 | paths.get(origin).put(firstNeighborAsn, Sets.newHashSet()); |
| 156 | } |
| 157 | } |
| 158 | } |
| 159 | }); |
Dimitrios Mavrommatis | f0c0632 | 2017-10-31 23:49:04 -0700 | [diff] [blame] | 160 | // else append to paths only the origin |
slowr | db071b2 | 2017-07-07 11:10:25 -0700 | [diff] [blame] | 161 | } else { |
| 162 | if (!paths.containsKey(origin)) { |
| 163 | paths.put(origin, Maps.newHashMap()); |
| 164 | } |
| 165 | } |
| 166 | } |
| 167 | |
Dimitrios Mavrommatis | f0c0632 | 2017-10-31 23:49:04 -0700 | [diff] [blame] | 168 | // /** |
| 169 | // * Helper function to print the loaded ASN paths. |
| 170 | // * |
| 171 | // * @param paths ASN paths to print |
| 172 | // */ |
| 173 | // private void printPaths(Map<Integer, Map<Integer, Set<Integer>>> paths) { |
| 174 | // log.warn("------------------------------------"); |
| 175 | // paths.forEach((k, v) -> v.forEach((l, n) -> { |
| 176 | // n.forEach(p -> log.warn("Origin: " + k + ", 1st: " + l + ", 2nd: " + p)); |
| 177 | // })); |
| 178 | // } |
slowr | db071b2 | 2017-07-07 11:10:25 -0700 | [diff] [blame] | 179 | |
| 180 | /** |
| 181 | * Gets the active route collectors. |
| 182 | * |
| 183 | * @return map with type as a key and host as a value. |
| 184 | */ |
| 185 | Map<String, Set<String>> activeMonitors() { |
| 186 | Map<String, Set<String>> monitors = Maps.newHashMap(); |
| 187 | |
Dimitrios Mavrommatis | f0c0632 | 2017-10-31 23:49:04 -0700 | [diff] [blame] | 188 | JsonNode monitorsNode = object.path(MONITORS); |
slowr | db071b2 | 2017-07-07 11:10:25 -0700 | [diff] [blame] | 189 | |
Dimitrios Mavrommatis | f0c0632 | 2017-10-31 23:49:04 -0700 | [diff] [blame] | 190 | if (!monitorsNode.isMissingNode()) { |
| 191 | JsonNode ripeNode = monitorsNode.path(RIPE); |
| 192 | if (!ripeNode.isMissingNode()) { |
| 193 | Set<String> hosts = Sets.newHashSet(); |
| 194 | ripeNode.forEach(host -> hosts.add(host.asText())); |
| 195 | monitors.put(RIPE, hosts); |
| 196 | } |
slowr | db071b2 | 2017-07-07 11:10:25 -0700 | [diff] [blame] | 197 | |
Dimitrios Mavrommatis | f0c0632 | 2017-10-31 23:49:04 -0700 | [diff] [blame] | 198 | JsonNode exabgpNode = monitorsNode.path(EXABGP); |
| 199 | if (!exabgpNode.isMissingNode()) { |
| 200 | Set<String> hosts = Sets.newHashSet(); |
| 201 | exabgpNode.forEach(host -> hosts.add(host.asText())); |
| 202 | monitors.put(EXABGP, hosts); |
| 203 | } |
slowr | db071b2 | 2017-07-07 11:10:25 -0700 | [diff] [blame] | 204 | } |
| 205 | |
| 206 | return monitors; |
| 207 | } |
| 208 | |
| 209 | /** |
Dimitrios Mavrommatis | f0c0632 | 2017-10-31 23:49:04 -0700 | [diff] [blame] | 210 | * Get the information about MOAS. Including remote MOAS server IPs, OVSDB ID and local tunnel IP. |
| 211 | * |
| 212 | * @return MOAS information |
| 213 | */ |
| 214 | MoasInfo moasInfo() { |
| 215 | MoasInfo moasInfo = new MoasInfo(); |
| 216 | |
| 217 | JsonNode moasNode = object.path(MOAS); |
| 218 | |
| 219 | if (!moasNode.isMissingNode()) { |
| 220 | JsonNode legitIpsNode = moasNode.path(MOAS_LEGIT); |
| 221 | if (!legitIpsNode.isMissingNode()) { |
| 222 | if (legitIpsNode.isArray()) { |
| 223 | moasInfo.setMoasAddresses( |
| 224 | Streams.stream(legitIpsNode) |
| 225 | .map(ipAddress -> IpAddress.valueOf(ipAddress.asText())) |
| 226 | .collect(Collectors.toSet()) |
| 227 | ); |
| 228 | } else { |
| 229 | log.warn("Legit MOAS field need to be a list"); |
| 230 | } |
| 231 | } else { |
| 232 | log.warn("No IPs for legit MOAS specified in configuration"); |
| 233 | } |
| 234 | |
| 235 | JsonNode tunnelPointsNode = moasNode.path(TUNNEL_POINTS); |
| 236 | if (!tunnelPointsNode.isMissingNode()) { |
| 237 | if (tunnelPointsNode.isArray()) { |
| 238 | tunnelPointsNode.forEach( |
| 239 | tunnelPoint -> { |
| 240 | JsonNode idNode = tunnelPoint.path(TUNNEL_OVSDB_IP), |
| 241 | localNode = tunnelPoint.path(TUNNEL_LOCAL_IP), |
| 242 | ovsNode = tunnelPoint.path(TUNNEL_OVS_PORT); |
| 243 | |
| 244 | if (!idNode.isMissingNode() && !localNode.isMissingNode()) { |
| 245 | moasInfo.addTunnelPoint( |
| 246 | new MoasInfo.TunnelPoint( |
| 247 | IpAddress.valueOf(idNode.asText()), |
| 248 | IpAddress.valueOf(localNode.asText()), |
| 249 | ovsNode.asText() |
| 250 | ) |
| 251 | ); |
| 252 | } else { |
| 253 | log.warn("Tunnel point need to have an ID and a Local IP"); |
| 254 | } |
| 255 | } |
| 256 | ); |
| 257 | } else { |
| 258 | log.warn("Tunnel points field need to be a list"); |
| 259 | } |
| 260 | } |
| 261 | } else { |
| 262 | log.warn("No tunnel points specified in configuration"); |
| 263 | } |
| 264 | |
| 265 | return moasInfo; |
| 266 | } |
| 267 | |
| 268 | /** |
| 269 | * Information holder for MOAS. |
| 270 | */ |
| 271 | public static class MoasInfo { |
| 272 | private Set<IpAddress> moasAddresses; |
| 273 | private Set<TunnelPoint> tunnelPoints; |
| 274 | |
| 275 | public MoasInfo() { |
| 276 | moasAddresses = Sets.newConcurrentHashSet(); |
| 277 | tunnelPoints = Sets.newConcurrentHashSet(); |
| 278 | } |
| 279 | |
| 280 | public Set<IpAddress> getMoasAddresses() { |
| 281 | return moasAddresses; |
| 282 | } |
| 283 | |
| 284 | public void setMoasAddresses(Set<IpAddress> moasAddresses) { |
| 285 | this.moasAddresses = moasAddresses; |
| 286 | } |
| 287 | |
| 288 | public Set<TunnelPoint> getTunnelPoints() { |
| 289 | return tunnelPoints; |
| 290 | } |
| 291 | |
| 292 | public void setTunnelPoints(Set<TunnelPoint> tunnelPoints) { |
| 293 | this.tunnelPoints = tunnelPoints; |
| 294 | } |
| 295 | |
| 296 | public TunnelPoint getTunnelPoint() { |
| 297 | return tunnelPoints.iterator().next(); |
| 298 | } |
| 299 | |
| 300 | public void addTunnelPoint(TunnelPoint tunnelPoint) { |
| 301 | this.tunnelPoints.add(tunnelPoint); |
| 302 | } |
| 303 | |
| 304 | @Override |
| 305 | public String toString() { |
| 306 | return "MoasInfo{" + |
| 307 | "moasAddresses=" + moasAddresses + |
| 308 | ", tunnelPoints=" + tunnelPoints + |
| 309 | '}'; |
| 310 | } |
| 311 | |
| 312 | public static class TunnelPoint { |
| 313 | private IpAddress ovsdbIp; |
| 314 | private IpAddress localIP; |
| 315 | private String ovsPort; |
| 316 | |
| 317 | public TunnelPoint(IpAddress ovsdbIp, IpAddress localIP, String ovsPort) { |
| 318 | this.ovsdbIp = ovsdbIp; |
| 319 | this.localIP = localIP; |
| 320 | this.ovsPort = ovsPort; |
| 321 | } |
| 322 | |
| 323 | public IpAddress getOvsdbIp() { |
| 324 | return ovsdbIp; |
| 325 | } |
| 326 | |
| 327 | public void setOvsdbIp(IpAddress ovsdbIp) { |
| 328 | this.ovsdbIp = ovsdbIp; |
| 329 | } |
| 330 | |
| 331 | public IpAddress getLocalIp() { |
| 332 | return localIP; |
| 333 | } |
| 334 | |
| 335 | public void setLocalIp(IpAddress localIP) { |
| 336 | this.localIP = localIP; |
| 337 | } |
| 338 | |
| 339 | public String getOvsPort() { |
| 340 | return ovsPort; |
| 341 | } |
| 342 | |
| 343 | public void setOvsPort(String ovsPort) { |
| 344 | this.ovsPort = ovsPort; |
| 345 | } |
| 346 | |
| 347 | @Override |
| 348 | public String toString() { |
| 349 | return "TunnelPoint{" + |
| 350 | "ovsdbIp='" + ovsdbIp + '\'' + |
| 351 | ", localIP=" + localIP + |
| 352 | ", ovsPort='" + ovsPort + '\'' + |
| 353 | '}'; |
| 354 | } |
| 355 | } |
| 356 | } |
| 357 | |
| 358 | /** |
slowr | db071b2 | 2017-07-07 11:10:25 -0700 | [diff] [blame] | 359 | * Configuration for a specific prefix. |
| 360 | */ |
Dimitrios Mavrommatis | f0c0632 | 2017-10-31 23:49:04 -0700 | [diff] [blame] | 361 | public class ArtemisPrefixes { |
slowr | db071b2 | 2017-07-07 11:10:25 -0700 | [diff] [blame] | 362 | private IpPrefix prefix; |
Dimitrios Mavrommatis | f0c0632 | 2017-10-31 23:49:04 -0700 | [diff] [blame] | 363 | private Set<IpAddress> moas; |
slowr | db071b2 | 2017-07-07 11:10:25 -0700 | [diff] [blame] | 364 | private Map<Integer, Map<Integer, Set<Integer>>> paths; |
| 365 | |
Dimitrios Mavrommatis | f0c0632 | 2017-10-31 23:49:04 -0700 | [diff] [blame] | 366 | ArtemisPrefixes(IpPrefix prefix, Set<IpAddress> moas, Map<Integer, Map<Integer, Set<Integer>>> paths) { |
slowr | db071b2 | 2017-07-07 11:10:25 -0700 | [diff] [blame] | 367 | this.prefix = checkNotNull(prefix); |
| 368 | this.moas = checkNotNull(moas); |
| 369 | this.paths = checkNotNull(paths); |
| 370 | } |
| 371 | |
| 372 | protected IpPrefix prefix() { |
| 373 | return prefix; |
| 374 | } |
| 375 | |
Dimitrios Mavrommatis | f0c0632 | 2017-10-31 23:49:04 -0700 | [diff] [blame] | 376 | protected Set<IpAddress> moas() { |
slowr | db071b2 | 2017-07-07 11:10:25 -0700 | [diff] [blame] | 377 | return moas; |
| 378 | } |
| 379 | |
| 380 | protected Map<Integer, Map<Integer, Set<Integer>>> paths() { |
| 381 | return paths; |
| 382 | } |
| 383 | |
| 384 | /** |
| 385 | * Given a path we check if the origin is a friendly MOAS or our ASN. |
| 386 | * If the origin ASN is not ours the we have a hijack of type 0. Next, in case that the first neighbor is |
| 387 | * not a legit neighbor from our configuration we detect a hijack of type 1 and lastly, if the second |
| 388 | * neighbor is not a legit neighbor we detect a type 2 hijack. |
| 389 | * |
| 390 | * @param path as-path that announces our prefix and found from monitors |
| 391 | * @return <code>0</code> no bgp hijack detected |
Dimitrios Mavrommatis | f0c0632 | 2017-10-31 23:49:04 -0700 | [diff] [blame] | 392 | * <code>50</code> friendly anycaster announcing our prefix |
| 393 | * <code>100+i</code> BGP hijack type i (0 <= i <=2) |
slowr | db071b2 | 2017-07-07 11:10:25 -0700 | [diff] [blame] | 394 | */ |
| 395 | int checkPath(JSONArray path) { |
| 396 | // TODO add MOAS check |
| 397 | ArrayList<Integer> asnPath = new ArrayList<>(); |
| 398 | for (int i = 0; i < path.length(); i++) { |
| 399 | try { |
| 400 | asnPath.add(path.getInt(i)); |
| 401 | } catch (JSONException e) { |
Ray Milkey | ba547f9 | 2018-02-01 15:22:31 -0800 | [diff] [blame] | 402 | log.warn("checkPath", e); |
slowr | db071b2 | 2017-07-07 11:10:25 -0700 | [diff] [blame] | 403 | } |
| 404 | } |
| 405 | // reverse the list to get path starting from origin |
| 406 | Collections.reverse(asnPath); |
| 407 | |
| 408 | if (asnPath.size() > 0 && !paths.containsKey(asnPath.get(0))) { |
| 409 | return 100; |
| 410 | } else if (asnPath.size() > 1 && !paths.get(asnPath.get(0)).containsKey(asnPath.get(1))) { |
| 411 | return 101; |
| 412 | } else if (asnPath.size() > 2 && !paths.get(asnPath.get(0)).get(asnPath.get(1)).contains(asnPath.get(2))) { |
| 413 | return 102; |
| 414 | } |
| 415 | return 0; |
| 416 | } |
| 417 | |
| 418 | @Override |
Dimitrios Mavrommatis | f0c0632 | 2017-10-31 23:49:04 -0700 | [diff] [blame] | 419 | public String toString() { |
| 420 | return "ArtemisPrefixes{" + |
| 421 | "prefix=" + prefix + |
| 422 | ", moas=" + moas + |
| 423 | ", paths=" + paths + |
| 424 | '}'; |
| 425 | } |
| 426 | |
| 427 | @Override |
slowr | db071b2 | 2017-07-07 11:10:25 -0700 | [diff] [blame] | 428 | public int hashCode() { |
| 429 | return Objects.hashCode(prefix); |
| 430 | } |
| 431 | |
| 432 | @Override |
| 433 | public boolean equals(Object obj) { |
| 434 | if (this == obj) { |
| 435 | return true; |
| 436 | } |
| 437 | if (obj instanceof PrefixHandler) { |
| 438 | final PrefixHandler that = (PrefixHandler) obj; |
| 439 | return Objects.equals(this.prefix, that.getPrefix()); |
| 440 | } |
| 441 | return false; |
| 442 | } |
| 443 | } |
| 444 | |
| 445 | } |