001 /** 002 * Copyright 2003-2005 Arthur van Hoff, Rick Blair 003 * 004 * Licensed to the Apache Software Foundation (ASF) under one or more 005 * contributor license agreements. See the NOTICE file distributed with 006 * this work for additional information regarding copyright ownership. 007 * The ASF licenses this file to You under the Apache License, Version 2.0 008 * (the "License"); you may not use this file except in compliance with 009 * the License. You may obtain a copy of the License at 010 * 011 * http://www.apache.org/licenses/LICENSE-2.0 012 * 013 * Unless required by applicable law or agreed to in writing, software 014 * distributed under the License is distributed on an "AS IS" BASIS, 015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 016 * See the License for the specific language governing permissions and 017 * limitations under the License. 018 */ 019 package org.apache.activemq.jmdns; 020 021 import java.io.ByteArrayOutputStream; 022 import java.io.IOException; 023 import java.io.OutputStream; 024 import java.net.InetAddress; 025 import java.util.Enumeration; 026 import java.util.Hashtable; 027 import java.util.TimerTask; 028 import java.util.Vector; 029 import java.util.logging.Logger; 030 031 /** 032 * JmDNS service information. 033 * 034 * @version %I%, %G% 035 * @author Arthur van Hoff, Jeff Sonstein, Werner Randelshofer 036 */ 037 public class ServiceInfo implements DNSListener 038 { 039 private static Logger logger = Logger.getLogger(ServiceInfo.class.toString()); 040 public final static byte[] NO_VALUE = new byte[0]; 041 JmDNS dns; 042 043 // State machine 044 /** 045 * The state of this service info. 046 * This is used only for services announced by JmDNS. 047 * <p/> 048 * For proper handling of concurrency, this variable must be 049 * changed only using methods advanceState(), revertState() and cancel(). 050 */ 051 private DNSState state = DNSState.PROBING_1; 052 053 /** 054 * Task associated to this service info. 055 * Possible tasks are JmDNS.Prober, JmDNS.Announcer, JmDNS.Responder, 056 * JmDNS.Canceler. 057 */ 058 TimerTask task; 059 060 String type; 061 private String name; 062 String server; 063 int port; 064 int weight; 065 int priority; 066 byte text[]; 067 Hashtable props; 068 InetAddress addr; 069 070 071 /** 072 * Construct a service description for registrating with JmDNS. 073 * 074 * @param type fully qualified service type name, such as <code>_http._tcp.local.</code>. 075 * @param name unqualified service instance name, such as <code>foobar</code> 076 * @param port the local port on which the service runs 077 * @param text string describing the service 078 */ 079 public ServiceInfo(String type, String name, int port, String text) 080 { 081 this(type, name, port, 0, 0, text); 082 } 083 084 /** 085 * Construct a service description for registrating with JmDNS. 086 * 087 * @param type fully qualified service type name, such as <code>_http._tcp.local.</code>. 088 * @param name unqualified service instance name, such as <code>foobar</code> 089 * @param port the local port on which the service runs 090 * @param weight weight of the service 091 * @param priority priority of the service 092 * @param text string describing the service 093 */ 094 public ServiceInfo(String type, String name, int port, int weight, int priority, String text) 095 { 096 this(type, name, port, weight, priority, (byte[]) null); 097 try 098 { 099 ByteArrayOutputStream out = new ByteArrayOutputStream(text.length()); 100 writeUTF(out, text); 101 this.text = out.toByteArray(); 102 } 103 catch (IOException e) 104 { 105 throw new RuntimeException("unexpected exception: " + e); 106 } 107 } 108 109 /** 110 * Construct a service description for registrating with JmDNS. The properties hashtable must 111 * map property names to either Strings or byte arrays describing the property values. 112 * 113 * @param type fully qualified service type name, such as <code>_http._tcp.local.</code>. 114 * @param name unqualified service instance name, such as <code>foobar</code> 115 * @param port the local port on which the service runs 116 * @param weight weight of the service 117 * @param priority priority of the service 118 * @param props properties describing the service 119 */ 120 public ServiceInfo(String type, String name, int port, int weight, int priority, Hashtable props) 121 { 122 this(type, name, port, weight, priority, new byte[0]); 123 if (props != null) 124 { 125 try 126 { 127 ByteArrayOutputStream out = new ByteArrayOutputStream(256); 128 for (Enumeration e = props.keys(); e.hasMoreElements();) 129 { 130 String key = (String) e.nextElement(); 131 Object val = props.get(key); 132 ByteArrayOutputStream out2 = new ByteArrayOutputStream(100); 133 writeUTF(out2, key); 134 if (val instanceof String) 135 { 136 out2.write('='); 137 writeUTF(out2, (String) val); 138 } 139 else 140 { 141 if (val instanceof byte[]) 142 { 143 out2.write('='); 144 byte[] bval = (byte[]) val; 145 out2.write(bval, 0, bval.length); 146 } 147 else 148 { 149 if (val != NO_VALUE) 150 { 151 throw new IllegalArgumentException("invalid property value: " + val); 152 } 153 } 154 } 155 byte data[] = out2.toByteArray(); 156 out.write(data.length); 157 out.write(data, 0, data.length); 158 } 159 this.text = out.toByteArray(); 160 } 161 catch (IOException e) 162 { 163 throw new RuntimeException("unexpected exception: " + e); 164 } 165 } 166 } 167 168 /** 169 * Construct a service description for registrating with JmDNS. 170 * 171 * @param type fully qualified service type name, such as <code>_http._tcp.local.</code>. 172 * @param name unqualified service instance name, such as <code>foobar</code> 173 * @param port the local port on which the service runs 174 * @param weight weight of the service 175 * @param priority priority of the service 176 * @param text bytes describing the service 177 */ 178 public ServiceInfo(String type, String name, int port, int weight, int priority, byte text[]) 179 { 180 this.type = type; 181 this.name = name; 182 this.port = port; 183 this.weight = weight; 184 this.priority = priority; 185 this.text = text; 186 } 187 188 /** 189 * Construct a service record during service discovery. 190 */ 191 ServiceInfo(String type, String name) 192 { 193 if (!type.endsWith(".")) 194 { 195 throw new IllegalArgumentException("type must be fully qualified DNS name ending in '.': " + type); 196 } 197 198 this.type = type; 199 this.name = name; 200 } 201 202 /** 203 * During recovery we need to duplicate service info to reregister them 204 */ 205 ServiceInfo(ServiceInfo info) 206 { 207 if (info != null) 208 { 209 this.type = info.type; 210 this.name = info.name; 211 this.port = info.port; 212 this.weight = info.weight; 213 this.priority = info.priority; 214 this.text = info.text; 215 } 216 } 217 218 /** 219 * Fully qualified service type name, such as <code>_http._tcp.local.</code> . 220 */ 221 public String getType() 222 { 223 return type; 224 } 225 226 /** 227 * Unqualified service instance name, such as <code>foobar</code> . 228 */ 229 public String getName() 230 { 231 return name; 232 } 233 234 /** 235 * Sets the service instance name. 236 * 237 * @param name unqualified service instance name, such as <code>foobar</code> 238 */ 239 void setName(String name) 240 { 241 this.name = name; 242 } 243 244 /** 245 * Fully qualified service name, such as <code>foobar._http._tcp.local.</code> . 246 */ 247 public String getQualifiedName() 248 { 249 return name + "." + type; 250 } 251 252 /** 253 * Get the name of the server. 254 */ 255 public String getServer() 256 { 257 return server; 258 } 259 260 /** 261 * Get the host address of the service (ie X.X.X.X). 262 */ 263 public String getHostAddress() 264 { 265 return (addr != null ? addr.getHostAddress() : ""); 266 } 267 268 public InetAddress getAddress() 269 { 270 return addr; 271 } 272 273 /** 274 * Get the InetAddress of the service. 275 */ 276 public InetAddress getInetAddress() 277 { 278 return addr; 279 } 280 281 /** 282 * Get the port for the service. 283 */ 284 public int getPort() 285 { 286 return port; 287 } 288 289 /** 290 * Get the priority of the service. 291 */ 292 public int getPriority() 293 { 294 return priority; 295 } 296 297 /** 298 * Get the weight of the service. 299 */ 300 public int getWeight() 301 { 302 return weight; 303 } 304 305 /** 306 * Get the text for the serivce as raw bytes. 307 */ 308 public byte[] getTextBytes() 309 { 310 return text; 311 } 312 313 /** 314 * Get the text for the service. This will interpret the text bytes 315 * as a UTF8 encoded string. Will return null if the bytes are not 316 * a valid UTF8 encoded string. 317 */ 318 public String getTextString() 319 { 320 if ((text == null) || (text.length == 0) || ((text.length == 1) && (text[0] == 0))) 321 { 322 return null; 323 } 324 return readUTF(text, 0, text.length); 325 } 326 327 /** 328 * Get the URL for this service. An http URL is created by 329 * combining the address, port, and path properties. 330 */ 331 public String getURL() 332 { 333 return getURL("http"); 334 } 335 336 /** 337 * Get the URL for this service. An URL is created by 338 * combining the protocol, address, port, and path properties. 339 */ 340 public String getURL(String protocol) 341 { 342 String url = protocol + "://" + getAddress() + ":" + getPort(); 343 String path = getPropertyString("path"); 344 if (path != null) 345 { 346 if (path.indexOf("://") >= 0) 347 { 348 url = path; 349 } 350 else 351 { 352 url += path.startsWith("/") ? path : "/" + path; 353 } 354 } 355 return url; 356 } 357 358 /** 359 * Get a property of the service. This involves decoding the 360 * text bytes into a property list. Returns null if the property 361 * is not found or the text data could not be decoded correctly. 362 */ 363 public synchronized byte[] getPropertyBytes(String name) 364 { 365 return (byte[]) getProperties().get(name); 366 } 367 368 /** 369 * Get a property of the service. This involves decoding the 370 * text bytes into a property list. Returns null if the property 371 * is not found, the text data could not be decoded correctly, or 372 * the resulting bytes are not a valid UTF8 string. 373 */ 374 public synchronized String getPropertyString(String name) 375 { 376 byte data[] = (byte[]) getProperties().get(name); 377 if (data == null) 378 { 379 return null; 380 } 381 if (data == NO_VALUE) 382 { 383 return "true"; 384 } 385 return readUTF(data, 0, data.length); 386 } 387 388 /** 389 * Enumeration of the property names. 390 */ 391 public Enumeration getPropertyNames() 392 { 393 Hashtable props = getProperties(); 394 return (props != null) ? props.keys() : new Vector().elements(); 395 } 396 397 /** 398 * Write a UTF string with a length to a stream. 399 */ 400 void writeUTF(OutputStream out, String str) throws IOException 401 { 402 for (int i = 0, len = str.length(); i < len; i++) 403 { 404 int c = str.charAt(i); 405 if ((c >= 0x0001) && (c <= 0x007F)) 406 { 407 out.write(c); 408 } 409 else 410 { 411 if (c > 0x07FF) 412 { 413 out.write(0xE0 | ((c >> 12) & 0x0F)); 414 out.write(0x80 | ((c >> 6) & 0x3F)); 415 out.write(0x80 | ((c >> 0) & 0x3F)); 416 } 417 else 418 { 419 out.write(0xC0 | ((c >> 6) & 0x1F)); 420 out.write(0x80 | ((c >> 0) & 0x3F)); 421 } 422 } 423 } 424 } 425 426 /** 427 * Read data bytes as a UTF stream. 428 */ 429 String readUTF(byte data[], int off, int len) 430 { 431 StringBuffer buf = new StringBuffer(); 432 for (int end = off + len; off < end;) 433 { 434 int ch = data[off++] & 0xFF; 435 switch (ch >> 4) 436 { 437 case 0: 438 case 1: 439 case 2: 440 case 3: 441 case 4: 442 case 5: 443 case 6: 444 case 7: 445 // 0xxxxxxx 446 break; 447 case 12: 448 case 13: 449 if (off >= len) 450 { 451 return null; 452 } 453 // 110x xxxx 10xx xxxx 454 ch = ((ch & 0x1F) << 6) | (data[off++] & 0x3F); 455 break; 456 case 14: 457 if (off + 2 >= len) 458 { 459 return null; 460 } 461 // 1110 xxxx 10xx xxxx 10xx xxxx 462 ch = ((ch & 0x0f) << 12) | ((data[off++] & 0x3F) << 6) | (data[off++] & 0x3F); 463 break; 464 default: 465 if (off + 1 >= len) 466 { 467 return null; 468 } 469 // 10xx xxxx, 1111 xxxx 470 ch = ((ch & 0x3F) << 4) | (data[off++] & 0x0f); 471 break; 472 } 473 buf.append((char) ch); 474 } 475 return buf.toString(); 476 } 477 478 synchronized Hashtable getProperties() 479 { 480 if ((props == null) && (text != null)) 481 { 482 Hashtable props = new Hashtable(); 483 int off = 0; 484 while (off < text.length) 485 { 486 // length of the next key value pair 487 int len = text[off++] & 0xFF; 488 if ((len == 0) || (off + len > text.length)) 489 { 490 props.clear(); 491 break; 492 } 493 // look for the '=' 494 int i = 0; 495 for (; (i < len) && (text[off + i] != '='); i++) 496 { 497 ; 498 } 499 500 // get the property name 501 String name = readUTF(text, off, i); 502 if (name == null) 503 { 504 props.clear(); 505 break; 506 } 507 if (i == len) 508 { 509 props.put(name, NO_VALUE); 510 } 511 else 512 { 513 byte value[] = new byte[len - ++i]; 514 System.arraycopy(text, off + i, value, 0, len - i); 515 props.put(name, value); 516 off += len; 517 } 518 } 519 this.props = props; 520 } 521 return props; 522 } 523 524 // REMIND: Oops, this shouldn't be public! 525 /** 526 * JmDNS callback to update a DNS record. 527 */ 528 public void updateRecord(JmDNS jmdns, long now, DNSRecord rec) 529 { 530 if ((rec != null) && !rec.isExpired(now)) 531 { 532 switch (rec.type) 533 { 534 case DNSConstants.TYPE_A: // IPv4 535 case DNSConstants.TYPE_AAAA: // IPv6 FIXME [PJYF Oct 14 2004] This has not been tested 536 if (rec.name.equals(server)) 537 { 538 addr = ((DNSRecord.Address) rec).getAddress(); 539 540 } 541 break; 542 case DNSConstants.TYPE_SRV: 543 if (rec.name.equals(getQualifiedName())) 544 { 545 DNSRecord.Service srv = (DNSRecord.Service) rec; 546 server = srv.server; 547 port = srv.port; 548 weight = srv.weight; 549 priority = srv.priority; 550 addr = null; 551 // changed to use getCache() instead - jeffs 552 // updateRecord(jmdns, now, (DNSRecord)jmdns.cache.get(server, TYPE_A, CLASS_IN)); 553 updateRecord(jmdns, now, (DNSRecord) jmdns.getCache().get(server, DNSConstants.TYPE_A, DNSConstants.CLASS_IN)); 554 } 555 break; 556 case DNSConstants.TYPE_TXT: 557 if (rec.name.equals(getQualifiedName())) 558 { 559 DNSRecord.Text txt = (DNSRecord.Text) rec; 560 text = txt.text; 561 } 562 break; 563 } 564 // Future Design Pattern 565 // This is done, to notify the wait loop in method 566 // JmDNS.getServiceInfo(type, name, timeout); 567 if (hasData() && dns != null) 568 { 569 dns.handleServiceResolved(this); 570 dns = null; 571 } 572 synchronized (this) 573 { 574 notifyAll(); 575 } 576 } 577 } 578 579 /** 580 * Returns true if the service info is filled with data. 581 */ 582 boolean hasData() 583 { 584 return server != null && addr != null && text != null; 585 } 586 587 588 // State machine 589 /** 590 * Sets the state and notifies all objects that wait on the ServiceInfo. 591 */ 592 synchronized void advanceState() 593 { 594 state = state.advance(); 595 notifyAll(); 596 } 597 598 /** 599 * Sets the state and notifies all objects that wait on the ServiceInfo. 600 */ 601 synchronized void revertState() 602 { 603 state = state.revert(); 604 notifyAll(); 605 } 606 607 /** 608 * Sets the state and notifies all objects that wait on the ServiceInfo. 609 */ 610 synchronized void cancel() 611 { 612 state = DNSState.CANCELED; 613 notifyAll(); 614 } 615 616 /** 617 * Returns the current state of this info. 618 */ 619 DNSState getState() 620 { 621 return state; 622 } 623 624 625 public int hashCode() 626 { 627 return getQualifiedName().hashCode(); 628 } 629 630 public boolean equals(Object obj) 631 { 632 return (obj instanceof ServiceInfo) && getQualifiedName().equals(((ServiceInfo) obj).getQualifiedName()); 633 } 634 635 public String getNiceTextString() 636 { 637 StringBuffer buf = new StringBuffer(); 638 for (int i = 0, len = text.length; i < len; i++) 639 { 640 if (i >= 20) 641 { 642 buf.append("..."); 643 break; 644 } 645 int ch = text[i] & 0xFF; 646 if ((ch < ' ') || (ch > 127)) 647 { 648 buf.append("\\0"); 649 buf.append(Integer.toString(ch, 8)); 650 } 651 else 652 { 653 buf.append((char) ch); 654 } 655 } 656 return buf.toString(); 657 } 658 659 public String toString() 660 { 661 StringBuffer buf = new StringBuffer(); 662 buf.append("service["); 663 buf.append(getQualifiedName()); 664 buf.append(','); 665 buf.append(getAddress()); 666 buf.append(':'); 667 buf.append(port); 668 buf.append(','); 669 buf.append(getNiceTextString()); 670 buf.append(']'); 671 return buf.toString(); 672 } 673 }