/*
 * $Id: TargetDescriptor.java,v 1.70 2012/03/03 21:23:44 agoubard Exp $
 *
 * See the COPYRIGHT file for redistribution and use restrictions.
 */
package org.xins.common.service;

import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.util.NoSuchElementException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.CRC32;

import org.xins.common.MandatoryArgumentChecker;
import org.xins.common.Utils;
import org.xins.common.text.HexConverter;
import org.xins.common.text.PatternUtils;

/**
 * Descriptor for a single target service. A target descriptor defines a URL
 * that identifies the location of the service. Also, it may define 3 kinds of
 * time-outs:
 *
 * <dl>
 *    <dt><em>total time-out</em> ({@link #getTotalTimeOut()})</dt>
 *    <dd>the maximum duration of a call, including connection time, time used
 *    to send the request, time used to receive the response, etc.</dd>
 *
 *    <dt><em>connection time-out</em> ({@link #getConnectionTimeOut()})</dt>
 *    <dd>the maximum time for attempting to establish a connection.</dd>
 *
 *    <dt><em>socket time-out</em> ({@link #getSocketTimeOut()})</dt>
 *    <dd>the maximum time for attempting to receive data on a socket.</dd>
 * </dl>
 *
 * @version $Revision: 1.70 $ $Date: 2012/03/03 21:23:44 $
 * @author <a href="mailto:ernst@ernstdehaan.com">Ernst de Haan</a>
 *
 * @since XINS 1.0.0
 */
public final class TargetDescriptor extends Descriptor {

   /**
    * The number of instances of this class. Initially 0.
    */
   private static int INSTANCE_COUNT;

   /**
    * The default time-out when no time-out is specified.
    */
   private static final int DEFAULT_TIMEOUT = 5000;

   /**
    * The pattern for a URL, as a character string.
    */
   private static final String PATTERN_STRING = "[a-z][a-z\\d]*(:[a-z\\d]+)*:\\/(\\/)?" +
           "([\\w%~\\.\\-]+(:[\\w%~\\.\\-]+)?@)?" +
           "[a-z\\d-]*(\\.[a-z\\d-]*)*(:[1-9][\\d]*)?" +
           "(\\/([a-zA-Z\\d%_~\\.\\-]*))*" +
           "(\\?([\\w%~\\.\\-]+=[\\w%~\\.\\-]*&?)*)?" +
           "(#[\\w%~\\.\\-]*)?";

   /**
    * The pattern for a URL.
    */
   private static Pattern PATTERN;

   /**
    * Computes the CRC-32 checksum for the specified character string.
    *
    * @param s
    *    the string for which to compute the checksum, not <code>null</code>.
    *
    * @return
    *    the checksum for <code>s</code>.
    */
   private static int computeCRC32(String s) {

      // Compute the CRC-32 checksum
      CRC32 checksum = new CRC32();
      byte[] bytes;
      final String ENCODING = "US-ASCII";
      try {
         bytes = s.getBytes(ENCODING);

      // Unsupported exception
      } catch (UnsupportedEncodingException exception) {
         throw Utils.logProgrammingError(exception);
      }

      checksum.update(bytes, 0, bytes.length);
      return (int) (checksum.getValue() & 0x00000000ffffffffL);
   }

   /**
    * The 1-based sequence number of this instance. Since this number is
    * 1-based, the first instance of this class will have instance number 1
    * assigned to it.
    */
   private final int _instanceNumber;

   /**
    * A textual representation of this object. Lazily initialized by
    * {@link #toString()} before returning it.
    */
   private String _asString;

   /**
    * The URL for the service. Cannot be <code>null</code>.
    */
   private final String _url;

   /**
    * The total time-out for the service. Is set to a 0 if no total time-out
    * should be applied.
    */
   private final int _timeOut;

   /**
    * The connection time-out for the service. Always greater than 0 and
    * smaller than or equal to the total time-out.
    */
   private final int _connectionTimeOut;

   /**
    * The socket time-out for the service. Always greater than 0 and smaller
    * than or equal to the total time-out.
    */
   private final int _socketTimeOut;

   /**
    * The CRC-32 checksum for the URL.
    */
   private final int _crc;

   /**
    * Constructs a new <code>TargetDescriptor</code> for the specified URL.
    *
    * <p>Note: Both the connection time-out and the socket time-out will be
    * set to the default time-out: 5 seconds.
    *
    * @param url
    *    the URL of the service, cannot be <code>null</code>.
    *
    * @throws IllegalArgumentException
    *    if <code>url == null</code>.
    *
    * @throws MalformedURLException
    *    if the specified URL is malformed.
    */
   public TargetDescriptor(String url)
   throws IllegalArgumentException, MalformedURLException {
      this(url, DEFAULT_TIMEOUT, DEFAULT_TIMEOUT, DEFAULT_TIMEOUT);
   }

   /**
    * Constructs a new <code>TargetDescriptor</code> for the specified URL,
    * with the specifed total time-out.
    *
    * <p>Note: Both the connection time-out and the socket time-out will be
    * set to equal the total time-out.
    *
    * @param url
    *    the URL of the service, cannot be <code>null</code>.
    *
    * @param timeOut
    *    the total time-out for the service, in milliseconds; or a
    *    non-positive value for no total time-out.
    *
    * @throws IllegalArgumentException
    *    if <code>url == null</code>.
    *
    * @throws MalformedURLException
    *    if the specified URL is malformed.
    */
   public TargetDescriptor(String url, int timeOut)
   throws IllegalArgumentException, MalformedURLException {
      this(url, timeOut, timeOut, timeOut);
   }

   /**
    * Constructs a new <code>TargetDescriptor</code> for the specified URL,
    * with the specifed total time-out and connection time-out.
    *
    * <p>Note: If the passed connection time-out is smaller than 1 ms, or
    * greater than the total time-out, then it will be adjusted to equal the
    * total time-out.
    *
    * <p>Note: The socket time-out will be set to equal the total time-out.
    *
    * @param url
    *    the URL of the service, cannot be <code>null</code>.
    *
    * @param timeOut
    *    the total time-out for the service, in milliseconds; or a
    *    non-positive value for no total time-out.
    *
    * @param connectionTimeOut
    *    the connection time-out for the service, in milliseconds; or a
    *    non-positive value if the connection time-out should equal the total
    *    time-out.
    *
    * @throws IllegalArgumentException
    *    if <code>url == null</code>.
    *
    * @throws MalformedURLException
    *    if the specified URL is malformed.
    */
   public TargetDescriptor(String url, int timeOut, int connectionTimeOut)
   throws IllegalArgumentException, MalformedURLException {
      this(url, timeOut, connectionTimeOut, timeOut);
   }

   /**
    * Constructs a new <code>TargetDescriptor</code> for the specified URL,
    * with the specifed total time-out, connection time-out and socket
    * time-out.
    *
    * <p>Note: If the passed connection time-out is smaller than 1 ms, or
    * greater than the total time-out, then it will be adjusted to equal the
    * total time-out.
    *
    * <p>Note: If the passed socket time-out is smaller than 1 ms or greater
    * than the total time-out, then it will be adjusted to equal the total
    * time-out.
    *
    * @param url
    *    the URL of the service, cannot be <code>null</code>.
    *
    * @param timeOut
    *    the total time-out for the service, in milliseconds; or a
    *    non-positive value for no total time-out.
    *
    * @param connectionTimeOut
    *    the connection time-out for the service, in milliseconds; or a
    *    non-positive value if the connection time-out should equal the total
    *    time-out.
    *
    * @param socketTimeOut
    *    the socket time-out for the service, in milliseconds; or a
    *    non-positive value for no socket time-out.
    *
    * @throws IllegalArgumentException
    *    if <code>url == null</code>.
    *
    * @throws MalformedURLException
    *    if the specified URL is malformed.
    */
   public TargetDescriptor(String url,
                           int    timeOut,
                           int    connectionTimeOut,
                           int    socketTimeOut)
   throws IllegalArgumentException, MalformedURLException {

      // Determine instance number first
      _instanceNumber = ++INSTANCE_COUNT;

      // Check preconditions
      MandatoryArgumentChecker.check("url", url);

      if (PATTERN == null) {
         PATTERN = PatternUtils.createPattern(PATTERN_STRING);
      }
      Matcher patternMatcher = PATTERN.matcher(url);
      if (! patternMatcher.matches()) {
         throw new MalformedURLException(url);
      }
      
      // Convert negative total time-out to 0
      timeOut = (timeOut > 0) ? timeOut : 0;

      // If connection time-out or socket time-out is not set, then set it to
      // the total time-out
      connectionTimeOut = (connectionTimeOut > 0) ? connectionTimeOut : timeOut;
      socketTimeOut     = (socketTimeOut     > 0) ? socketTimeOut     : timeOut;

      // If either connection or socket time-out is greater than total
      // time-out, then limit it to the total time-out
      connectionTimeOut = (connectionTimeOut < timeOut) ? connectionTimeOut : timeOut;
      socketTimeOut     = (socketTimeOut     < timeOut) ? socketTimeOut     : timeOut;

      // Set fields
      _url               = url;
      _timeOut           = timeOut;
      _connectionTimeOut = connectionTimeOut;
      _socketTimeOut     = socketTimeOut;
      _crc               = computeCRC32(url);

      // NOTE: _asString is lazily initialized
   }

   /**
    * Checks if this descriptor denotes a group of descriptors.
    *
    * @return
    *    <code>false</code>, since this descriptor does not denote a group.
    */
   public boolean isGroup() {
      return false;
   }

   /**
    * Returns the URL for the service.
    *
    * @return
    *    the URL for the service, not <code>null</code>.
    */
   public String getURL() {
      return _url;
   }

   /**
    * Returns the protocol in the URL for the service.
    *
    * @return
    *    the protocol in the URL, not <code>null</code>.
    *
    * @since XINS 1.2.0
    */
   public String getProtocol() {
      int index = _url.indexOf(":/");
      return _url.substring(0, index);
   }

   /**
    * Returns the total time-out for a call to the service. The value 0
    * is returned if there is no total time-out.
    *
    * @return
    *    the total time-out for the service, as a positive number, in
    *    milli-seconds, or 0 if there is no total time-out.
    */
   public int getTotalTimeOut() {
      return _timeOut;
   }

   /**
    * Returns the connection time-out for a call to the service.
    *
    * @return
    *    the connection time-out for the service; always greater than 0 and
    *    smaller than or equal to the total time-out.
    */
   public int getConnectionTimeOut() {
      return _connectionTimeOut;
   }

   /**
    * Returns the socket time-out for a call to the service.
    *
    * @return
    *    the socket time-out for the service; always greater than 0 and
    *    smaller than or equal to the total time-out.
    */
   public int getSocketTimeOut() {
      return _socketTimeOut;
   }

   /**
    * Returns the CRC-32 checksum for the URL of this target descriptor.
    *
    * @return
    *    the CRC-32 checksum.
    */
   public int getCRC() {
      return _crc;
   }

   /**
    * Iterates over all leaves, the target descriptors.
    *
    * <p>The returned {@link java.util.Iterator} will only return this target
    * descriptor.
    *
    * @return
    *    iterator that returns this target descriptor, never
    *    <code>null</code>.
    */
   
   public java.util.Iterator iterateTargets() {
      return new Iterator();
   }

   public java.util.Iterator<TargetDescriptor> iterator() {
      return new Iterator();
   }

   /**
    * Counts the total number of target descriptors in/under this descriptor.
    *
    * @return
    *    the total number of target descriptors, always 1.
    */
   public int getTargetCount() {
      return 1;
   }

   /**
    * Returns the <code>TargetDescriptor</code> that matches the specified
    * CRC-32 checksum.
    *
    * @param crc
    *    the CRC-32 checksum.
    *
    * @return
    *    the {@link TargetDescriptor} that matches the specified checksum, or
    *    <code>null</code>, if none could be found in this descriptor.
    */
   public TargetDescriptor getTargetByCRC(int crc) {
      return (_crc == crc) ? this : null;
   }

   /**
    * Returns a hash code value for the object.
    *
    * @return
    *    a hash code value for this object.
    *
    * @see Object#hashCode()
    * @see #equals(Object)
    */
   public int hashCode() {
       return _crc;
   }

   /**
    * Indicates whether some other object is "equal to" this one. This method
    * considers <code>obj</code> equals if and only if it matches the
    * following conditions:
    *
    * <ul>
    *    <li><code>obj instanceof TargetDescriptor</code>
    *    <li>URL is equal
    *    <li>total time-out is equal
    *    <li>connection time-out is equal
    *    <li>socket time-out is equal
    * </ul>
    *
    * @param obj
    *    the reference object with which to compare.
    *
    * @return
    *    <code>true</code> if this object is the same as the <code>obj</code>
    *    argument; <code>false</code> otherwise.
    *
    * @see #hashCode()
    */
   public boolean equals(Object obj) {

      boolean equal = false;

      if (obj instanceof TargetDescriptor) {
         TargetDescriptor that = (TargetDescriptor) obj;
         equal = (_url.equals(that._url))
              && (_timeOut           == that._timeOut)
              && (_connectionTimeOut == that._connectionTimeOut)
              && (_socketTimeOut     == that._socketTimeOut);
      }

      return equal;
   }

   /**
    * Textual description of this object. The string includes the URL and all
    * time-out values. For example:
    *
    * <blockquote><code>TargetDescriptor(url="http://api.google.com/some_api/";
    * total-time-out is 5300 ms;
    * connection time-out is 1000 ms;
    * socket time-out is disabled)</code></blockquote>
    *
    * @return
    *    this <code>TargetDescriptor</code> as a {@link String}, never
    *    <code>null</code>.
    */
   public String toString() {

      // Lazily initialize
      if (_asString == null) {
         StringBuffer buffer = new StringBuffer(233);
         buffer.append("TargetDescriptor #");
         buffer.append(_instanceNumber);
         buffer.append(" [url=\"");
         buffer.append(_url);
         buffer.append("\"; crc=\"");
         buffer.append(HexConverter.toHexString(_crc));
         buffer.append("\"; total time-out is ");
         if (_timeOut < 1) {
            buffer.append("disabled; connection time-out is ");
         } else {
            buffer.append(_timeOut);
            buffer.append(" ms; connection time-out is ");
         }
         if (_connectionTimeOut < 1) {
            buffer.append("disabled; socket time-out is ");
         } else {
            buffer.append(_connectionTimeOut);
            buffer.append(" ms; socket time-out is ");
         }
         if (_socketTimeOut < 1) {
            buffer.append("disabled]");
         } else {
            buffer.append(_socketTimeOut);
            buffer.append(" ms]");
         }
         _asString = buffer.toString();
      }

      return _asString;
   }

   /**
    * Iterator over this (single) target descriptor. Needed for the
    * implementation of {@link #iterateTargets()}.
    *
    * @version $Revision: 1.70 $ $Date: 2012/03/03 21:23:44 $
    * @author <a href="mailto:ernst@ernstdehaan.com">Ernst de Haan</a>
    *
    * @since XINS 1.0.0
    */
   private class Iterator implements java.util.Iterator {
      /**
       * Constructs a new <code>Iterator</code>.
       */
      private Iterator() {

         // empty
      }

      /**
       * Flag that indicates if this iterator is already done iterating over
       * the single element.
       */
      private boolean _done;

      /**
       * Checks if there is a next element.
       *
       * @return
       *    <code>true</code> if there is a next element, <code>false</code>
       *    if there is not.
       */
      public boolean hasNext() {
         return ! _done;
      }

      /**
       * Returns the next element.
       *
       * @return
       *    the next element, never <code>null</code>.
       *
       * @throws NoSuchElementException
       *    if there is no new element.
       */
      public Object next() throws NoSuchElementException {
         if (_done) {
            throw new NoSuchElementException();
         } else {
            _done = true;
            return TargetDescriptor.this;
         }
      }

      /**
       * Removes the element last returned by <code>next()</code> (unsupported
       * operation).
       *
       * @throws UnsupportedOperationException
       *    always thrown, since this operation is unsupported.
       */
      public void remove() throws UnsupportedOperationException {
         throw new UnsupportedOperationException();
      }
   }
}