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 }