All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.netxms.client.NXCSession Maven / Gradle / Ivy

There is a newer version: 5.0.6
Show newest version
/**
 * NetXMS - open source network management system
 * Copyright (C) 2003-2017 Victor Kirhenshtein
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */
package org.netxms.client;

import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Writer;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import java.security.GeneralSecurityException;
import java.security.Signature;
import java.security.SignatureException;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.netxms.base.EncryptionContext;
import org.netxms.base.GeoLocation;
import org.netxms.base.InetAddressEx;
import org.netxms.base.Logger;
import org.netxms.base.MacAddress;
import org.netxms.base.NXCPCodes;
import org.netxms.base.NXCPDataInputStream;
import org.netxms.base.NXCPException;
import org.netxms.base.NXCPMessage;
import org.netxms.base.NXCPMessageReceiver;
import org.netxms.base.NXCPMsgWaitQueue;
import org.netxms.base.NXCommon;
import org.netxms.client.agent.config.ConfigContent;
import org.netxms.client.agent.config.ConfigListElement;
import org.netxms.client.constants.AggregationFunction;
import org.netxms.client.constants.AuthenticationType;
import org.netxms.client.constants.NodePollType;
import org.netxms.client.constants.ObjectStatus;
import org.netxms.client.constants.RCC;
import org.netxms.client.dashboards.DashboardElement;
import org.netxms.client.datacollection.ConditionDciInfo;
import org.netxms.client.datacollection.DataCollectionConfiguration;
import org.netxms.client.datacollection.DataCollectionItem;
import org.netxms.client.datacollection.DataCollectionObject;
import org.netxms.client.datacollection.DciData;
import org.netxms.client.datacollection.DciDataRow;
import org.netxms.client.datacollection.DciPushData;
import org.netxms.client.datacollection.DciSummaryTable;
import org.netxms.client.datacollection.DciSummaryTableColumn;
import org.netxms.client.datacollection.DciSummaryTableDescriptor;
import org.netxms.client.datacollection.DciValue;
import org.netxms.client.datacollection.GraphSettings;
import org.netxms.client.datacollection.PerfTabDci;
import org.netxms.client.datacollection.PredictionEngine;
import org.netxms.client.datacollection.SimpleDciValue;
import org.netxms.client.datacollection.Threshold;
import org.netxms.client.datacollection.ThresholdViolationSummary;
import org.netxms.client.datacollection.TransformationTestResult;
import org.netxms.client.datacollection.WinPerfObject;
import org.netxms.client.events.Alarm;
import org.netxms.client.events.AlarmCategory;
import org.netxms.client.events.AlarmComment;
import org.netxms.client.events.BulkAlarmStateChangeData;
import org.netxms.client.events.Event;
import org.netxms.client.events.EventInfo;
import org.netxms.client.events.EventProcessingPolicy;
import org.netxms.client.events.EventProcessingPolicyRule;
import org.netxms.client.events.EventTemplate;
import org.netxms.client.events.SyslogRecord;
import org.netxms.client.log.Log;
import org.netxms.client.maps.MapDCIInstance;
import org.netxms.client.maps.NetworkMapLink;
import org.netxms.client.maps.NetworkMapPage;
import org.netxms.client.maps.configs.SingleDciConfig;
import org.netxms.client.maps.elements.NetworkMapElement;
import org.netxms.client.maps.elements.NetworkMapObject;
import org.netxms.client.market.Repository;
import org.netxms.client.mt.MappingTable;
import org.netxms.client.mt.MappingTableDescriptor;
import org.netxms.client.objects.AbstractNode;
import org.netxms.client.objects.AbstractObject;
import org.netxms.client.objects.AccessPoint;
import org.netxms.client.objects.AgentPolicy;
import org.netxms.client.objects.AgentPolicyConfig;
import org.netxms.client.objects.AgentPolicyLogParser;
import org.netxms.client.objects.BusinessService;
import org.netxms.client.objects.BusinessServiceRoot;
import org.netxms.client.objects.Chassis;
import org.netxms.client.objects.Cluster;
import org.netxms.client.objects.ClusterResource;
import org.netxms.client.objects.Condition;
import org.netxms.client.objects.Container;
import org.netxms.client.objects.Dashboard;
import org.netxms.client.objects.DashboardGroup;
import org.netxms.client.objects.DashboardRoot;
import org.netxms.client.objects.EntireNetwork;
import org.netxms.client.objects.GenericObject;
import org.netxms.client.objects.Interface;
import org.netxms.client.objects.MobileDevice;
import org.netxms.client.objects.NetworkMap;
import org.netxms.client.objects.NetworkMapGroup;
import org.netxms.client.objects.NetworkMapRoot;
import org.netxms.client.objects.NetworkService;
import org.netxms.client.objects.Node;
import org.netxms.client.objects.NodeLink;
import org.netxms.client.objects.PolicyGroup;
import org.netxms.client.objects.PolicyRoot;
import org.netxms.client.objects.Rack;
import org.netxms.client.objects.ServiceCheck;
import org.netxms.client.objects.ServiceRoot;
import org.netxms.client.objects.Subnet;
import org.netxms.client.objects.Template;
import org.netxms.client.objects.TemplateGroup;
import org.netxms.client.objects.TemplateRoot;
import org.netxms.client.objects.UnknownObject;
import org.netxms.client.objects.VPNConnector;
import org.netxms.client.objects.Zone;
import org.netxms.client.objecttools.ObjectTool;
import org.netxms.client.objecttools.ObjectToolDetails;
import org.netxms.client.packages.PackageDeploymentListener;
import org.netxms.client.packages.PackageInfo;
import org.netxms.client.reporting.ReportDefinition;
import org.netxms.client.reporting.ReportRenderFormat;
import org.netxms.client.reporting.ReportResult;
import org.netxms.client.reporting.ReportingJob;
import org.netxms.client.server.AgentFile;
import org.netxms.client.server.ServerConsoleListener;
import org.netxms.client.server.ServerFile;
import org.netxms.client.server.ServerJob;
import org.netxms.client.server.ServerJobIdUpdater;
import org.netxms.client.server.ServerVariable;
import org.netxms.client.snmp.SnmpTrap;
import org.netxms.client.snmp.SnmpTrapLogRecord;
import org.netxms.client.snmp.SnmpUsmCredential;
import org.netxms.client.snmp.SnmpValue;
import org.netxms.client.snmp.SnmpWalkListener;
import org.netxms.client.topology.ConnectionPoint;
import org.netxms.client.topology.FdbEntry;
import org.netxms.client.topology.NetworkPath;
import org.netxms.client.topology.Route;
import org.netxms.client.topology.VlanInfo;
import org.netxms.client.topology.WirelessStation;
import org.netxms.client.users.AbstractUserObject;
import org.netxms.client.users.AuthCertificate;
import org.netxms.client.users.User;
import org.netxms.client.users.UserGroup;
import org.netxms.client.zeromq.ZmqSubscription;
import org.netxms.client.zeromq.ZmqSubscriptionType;
import com.jcraft.jzlib.Deflater;
import com.jcraft.jzlib.JZlib;

/**
 * Communication session with NetXMS server.
 */
public class NXCSession
{
   // Various public constants
   public static final int DEFAULT_CONN_PORT = 4701;

   // Core notification channels
   public static final String CHANNEL_EVENTS = "Core.Events";
   public static final String CHANNEL_SYSLOG = "Core.Syslog";
   public static final String CHANNEL_ALARMS = "Core.Alarms";
   public static final String CHANNEL_OBJECTS = "Core.Objects";
   public static final String CHANNEL_SNMP_TRAPS = "Core.SNMP.Traps";
   public static final String CHANNEL_AUDIT_LOG = "Core.Audit";
   public static final String CHANNEL_USERDB = "Core.UserDB";

   // Object sync options
   public static final int OBJECT_SYNC_NOTIFY = 0x0001;
   public static final int OBJECT_SYNC_WAIT = 0x0002;

   // Configuration import options
   public static final int CFG_IMPORT_REPLACE_EVENT_BY_CODE = 0x0001;
   public static final int CFG_IMPORT_REPLACE_EVENT_BY_NAME = 0x0002;

   // Address list IDs
   public static final int ADDRESS_LIST_DISCOVERY_TARGETS = 1;
   public static final int ADDRESS_LIST_DISCOVERY_FILTER = 2;

   // Server components
   public static final int SERVER_COMPONENT_DISCOVERY_MANAGER = 1;

   // Client types
   public static final int DESKTOP_CLIENT = 0;
   public static final int WEB_CLIENT = 1;
   public static final int MOBILE_CLIENT = 2;
   public static final int TABLET_CLIENT = 3;
   public static final int APPLICATION_CLIENT = 4;

   // Private constants
   private static final int CLIENT_CHALLENGE_SIZE = 256;
   private static final int MAX_DCI_DATA_ROWS = 200000;
   private static final int MAX_DCI_STRING_VALUE_LENGTH = 256;
   private static final int RECEIVED_FILE_TTL = 300000; // 300 seconds
   private static final int FILE_BUFFER_SIZE = 32768; // 32KB

   // Internal synchronization objects
   private final Semaphore syncObjects = new Semaphore(1);
   private final Semaphore syncUserDB = new Semaphore(1);

   // Connection-related attributes
   private String connAddress;
   private int connPort;
   private boolean connUseEncryption;
   private String connClientInfo = "nxjclient/" + NXCommon.VERSION;
   private int clientType = DESKTOP_CLIENT;
   private String clientAddress = null;
   private boolean ignoreProtocolVersion = false;
   private String clientLanguage = "en";

   // Information about logged in user
   private int sessionId;
   private int userId;
   private String userName;
   private AuthenticationType authenticationMethod;
   private long userSystemRights;
   private boolean passwordExpired;
   private int graceLogins;

   // Internal communication data
   private Socket socket = null;
   private NXCPMsgWaitQueue msgWaitQueue = null;
   private ReceiverThread recvThread = null;
   private HousekeeperThread housekeeperThread = null;
   private AtomicLong requestId = new AtomicLong(1);
   private boolean connected = false;
   private boolean disconnected = false;
   private boolean serverConsoleConnected = false;
   private boolean allowCompression = false;
   private EncryptionContext encryptionContext = null;

   // Communication parameters
   private int defaultRecvBufferSize = 4194304; // Default is 4MB
   private int maxRecvBufferSize = 33554432;    // Max is 32MB
   private int connectTimeout = 10000; // Default is 10 seconds  
   private int commandTimeout = 30000; // Default is 30 seconds
   private int serverCommandOutputTimeout = 60000;

   // Notification listeners and queue
   private LinkedBlockingQueue notificationQueue = new LinkedBlockingQueue(8192);
   private Set listeners = new HashSet(0);
   private Set consoleListeners = new HashSet(0);
   private Map progressListeners = new HashMap(0);
   
   // Message subscriptions
   private Map messageSubscriptions = new HashMap(0);

   // Received files
   private Map receivedFiles = new HashMap();
   
   // Received file updates(for file monitoring)
   private Map receivedFileUpdates = new HashMap();

   // Server information
   private ProtocolVersion protocolVersion;
   private String serverVersion = "(unknown)";
   private long serverId = 0;
   private String serverTimeZone;
   private byte[] serverChallenge = new byte[CLIENT_CHALLENGE_SIZE];
   private boolean zoningEnabled = false;
   private boolean helpdeskLinkActive = false;
   private String tileServerURL;
   private String dateFormat;
   private String timeFormat;
   private String shortTimeFormat;
   private int defaultDciRetentionTime;
   private int defaultDciPollingInterval;
   private boolean strictAlarmStatusFlow;
   private boolean timedAlarmAckEnabled;
   private int minViewRefreshInterval;
   private long serverTime = System.currentTimeMillis();
   private long serverTimeRecvTime = System.currentTimeMillis();
   private Set serverComponents = new HashSet(0);
   private String serverName;
   private String serverColor;

   // Configuration hints
   private Map clientConfigurationHints = new HashMap();
   
   // Objects
   private Map objectList = new HashMap();
   private Map objectListGUID = new HashMap();
   private Map zoneList = new HashMap();
   private boolean objectsSynchronized = false;

   // Users
   private Map userDB = new HashMap();

   // Event templates
   private Map eventTemplates = new HashMap();
   private boolean eventTemplatesNeedSync = false;
   
   // Alarm categories
   private Map alarmCategories = new HashMap();
   private boolean alarmCategoriesNeedSync = false;
   
   // Message of the day
   private String messageOfTheDay;
   
   /**
    * Message subscription class
    */
   final protected class MessageSubscription
   {
      protected int messageCode;
      protected long messageId;
      
      /**
       * @param messageCode The message code
       * @param messageId The message ID
       */
      protected MessageSubscription(int messageCode, long messageId)
      {
         this.messageCode = messageCode;
         this.messageId = messageId;
      }
      
      /* (non-Javadoc)
       * @see java.lang.Object#hashCode()
       */
      @Override
      public int hashCode()
      {
         final int prime = 31;
         int result = 1;
         result = prime * result + messageCode;
         result = prime * result + (int)(messageId ^ (messageId >>> 32));
         return result;
      }

      /* (non-Javadoc)
       * @see java.lang.Object#equals(java.lang.Object)
       */
      @Override
      public boolean equals(Object obj)
      {
         if (this == obj)
            return true;
         if (obj == null)
            return false;
         if (getClass() != obj.getClass())
            return false;
         MessageSubscription other = (MessageSubscription)obj;
         if (messageCode != other.messageCode)
            return false;
         if (messageId != other.messageId)
            return false;
         return true;
      }
   }

   /**
    * Receiver thread for NXCSession
    */
   private class ReceiverThread extends Thread
   {
      ReceiverThread()
      {
         setDaemon(true);
         start();
      }

      @Override
      public void run()
      {
         final NXCPMessageReceiver receiver = new NXCPMessageReceiver(defaultRecvBufferSize, maxRecvBufferSize);
         InputStream in;

         try
         {
            in = socket.getInputStream();
         }
         catch(IOException e)
         {
            return; // Stop receiver thread if input stream cannot be obtained
         }

         while(socket.isConnected())
         {
            try
            {
               NXCPMessage msg = receiver.receiveMessage(in, encryptionContext);
               switch(msg.getMessageCode())
               {
                  case NXCPCodes.CMD_REQUEST_SESSION_KEY:
                     setupEncryption(msg);
                     break;
                  case NXCPCodes.CMD_KEEPALIVE:
                     serverTime = msg.getFieldAsInt64(NXCPCodes.VID_TIMESTAMP) * 1000;
                     serverTimeRecvTime = System.currentTimeMillis();
                     break;
                  case NXCPCodes.CMD_OBJECT:
                  case NXCPCodes.CMD_OBJECT_UPDATE:
                     if (!msg.getFieldAsBoolean(NXCPCodes.VID_IS_DELETED))
                     {
                        final AbstractObject obj = createObjectFromMessage(msg);
                        synchronized(objectList)
                        {
                           objectList.put(obj.getObjectId(), obj);
                           objectListGUID.put(obj.getGuid(), obj);
                           if (obj instanceof Zone)
                              zoneList.put(((Zone)obj).getUIN(), (Zone)obj);
                        }
                        if (msg.getMessageCode() == NXCPCodes.CMD_OBJECT_UPDATE)
                        {
                           sendNotification(new SessionNotification(SessionNotification.OBJECT_CHANGED, obj.getObjectId(), obj));
                        }
                     }
                     else
                     {
                        long objectId = msg.getFieldAsInt32(NXCPCodes.VID_OBJECT_ID);
                        synchronized(objectList)
                        {
                           AbstractObject object = objectList.get(objectId);
                           if (object != null)
                           {
                              objectListGUID.remove(object.getGuid());
                              objectList.remove(objectId);
                              if (object instanceof Zone)
                                 zoneList.remove(((Zone)object).getUIN());
                           }
                        }
                        sendNotification(new SessionNotification(SessionNotification.OBJECT_DELETED, objectId));
                     }
                     break;
                  case NXCPCodes.CMD_OBJECT_LIST_END:
                     completeSync(syncObjects);
                     break;
                  case NXCPCodes.CMD_USER_DATA:
                     final User user = new User(msg);
                     synchronized(userDB)
                     {
                        if (user.isDeleted())
                        {
                           userDB.remove(user.getId());
                        }
                        else
                        {
                           userDB.put(user.getId(), user);
                        }
                     }
                     break;
                  case NXCPCodes.CMD_GROUP_DATA:
                     final UserGroup group = new UserGroup(msg);
                     synchronized(userDB)
                     {
                        if (group.isDeleted())
                        {
                           userDB.remove(group.getId());
                        }
                        else
                        {
                           userDB.put(group.getId(), group);
                        }
                     }
                     break;
                  case NXCPCodes.CMD_USER_DB_EOF:
                     completeSync(syncUserDB);
                     break;
                  case NXCPCodes.CMD_USER_DB_UPDATE:
                     processUserDBUpdate(msg);
                     break;
                  case NXCPCodes.CMD_ALARM_UPDATE:
                     sendNotification(new SessionNotification(
                        msg.getFieldAsInt32(NXCPCodes.VID_NOTIFICATION_CODE) + SessionNotification.NOTIFY_BASE,
                        new Alarm(msg)));
                     break;
                  case NXCPCodes.CMD_BULK_ALARM_STATE_CHANGE:
                     processBulkAlarmStateChange(msg);
                     break;
                  case NXCPCodes.CMD_JOB_CHANGE_NOTIFICATION:
                     sendNotification(new SessionNotification(SessionNotification.JOB_CHANGE, new ServerJob(msg)));
                     break;
                  case NXCPCodes.CMD_FILE_DATA:
                     processFileData(msg);
                     break;
                  case NXCPCodes.CMD_FILE_MONITORING:
                     processFileTail(msg);
                     break;
                  case NXCPCodes.CMD_ABORT_FILE_TRANSFER:
                     processFileTransferError(msg);
                     break;
                  case NXCPCodes.CMD_NOTIFY:
                     processNotificationMessage(msg, true);
                     break;
                  case NXCPCodes.CMD_RS_NOTIFY:
                     processNotificationMessage(msg, false);
                     break;
                  case NXCPCodes.CMD_EVENTLOG_RECORDS:
                     processNewEvents(msg);
                     break;
                  case NXCPCodes.CMD_TRAP_LOG_RECORDS:
                     processNewTraps(msg);
                     break;
                  case NXCPCodes.CMD_SYSLOG_RECORDS:
                     processSyslogRecords(msg);
                     break;
                  case NXCPCodes.CMD_ACTION_DB_UPDATE:
                     processActionConfigChange(msg);
                     break;
                  case NXCPCodes.CMD_EVENT_DB_UPDATE:
                     processEventConfigChange(msg);
                     break;
                  case NXCPCodes.CMD_TRAP_CFG_UPDATE:
                     processTrapConfigChange(msg);
                     break;
                  case NXCPCodes.CMD_ADM_MESSAGE:
                     processConsoleOutput(msg);
                     break;
                  case NXCPCodes.CMD_IMAGE_LIBRARY_UPDATE:
                     processImageLibraryUpdate(msg);
                     break;
                  case NXCPCodes.CMD_GRAPH_UPDATE:
                     GraphSettings graph = GraphSettings.createGraphSettings(msg, NXCPCodes.VID_GRAPH_LIST_BASE);
                     sendNotification(new SessionNotification(SessionNotification.PREDEFINED_GRAPHS_CHANGED, graph.getId(), graph));
                     break;
                  case NXCPCodes.CMD_ALARM_CATEGORY_UPDATE:
                     processAlarmCategoryConfigChange(msg);
                     break;
                  default:
                     // Check subscriptions
                     synchronized(messageSubscriptions)
                     {
                        MessageSubscription s = new MessageSubscription(msg.getMessageCode(), msg.getMessageId());
                        MessageHandler handler = messageSubscriptions.get(s);
                        if (handler != null)
                        { 
                           if (handler.processMessage(msg))
                              msg = null;
                           if (handler.isComplete())
                              messageSubscriptions.remove(s);
                           else
                              handler.setLastMessageTimestamp(System.currentTimeMillis());
                        }
                     }
                     if (msg != null)
                     {
                        if (msg.getMessageCode() >= 0x1000)
                        {
                           // Custom message
                           sendNotification(new SessionNotification(SessionNotification.CUSTOM_MESSAGE, msg));
                        }
                        msgWaitQueue.putMessage(msg);
                     }
                     break;
               }
            }
            catch(IOException e)
            {
               break; // Stop on read errors
            }
            catch(NXCPException e)
            {
               if (e.getErrorCode() == NXCPException.SESSION_CLOSED) break;
            }
            catch(NXCException e)
            {
               if (e.getErrorCode() == RCC.ENCRYPTION_ERROR) break;
            }
         }
      }

      /**
       * Process server console output
       *
       * @param msg notification message
       */
      private void processConsoleOutput(NXCPMessage msg)
      {
         final String text = msg.getFieldAsString(NXCPCodes.VID_MESSAGE);
         synchronized(consoleListeners)
         {
            for(ServerConsoleListener l : consoleListeners)
            {
               l.onConsoleOutput(text);
            }
         }
      }

      /**
       * Process event notification messages.
       *
       * @param msg NXCP message
       */
      private void processNewEvents(final NXCPMessage msg)
      {
         int count = msg.getFieldAsInt32(NXCPCodes.VID_NUM_RECORDS);
         int order = msg.getFieldAsInt32(NXCPCodes.VID_RECORDS_ORDER);
         long varId = NXCPCodes.VID_EVENTLOG_MSG_BASE;
         for(int i = 0; i < count; i++)
         {
            Event event = new Event(msg, varId);
            sendNotification(new SessionNotification(SessionNotification.NEW_EVENTLOG_RECORD, order, event));
         }
      }

      /**
       * Process SNMP trap notification messages.
       *
       * @param msg NXCP message
       */
      private void processNewTraps(final NXCPMessage msg)
      {
         int count = msg.getFieldAsInt32(NXCPCodes.VID_NUM_RECORDS);
         int order = msg.getFieldAsInt32(NXCPCodes.VID_RECORDS_ORDER);
         long varId = NXCPCodes.VID_TRAP_LOG_MSG_BASE;
         for(int i = 0; i < count; i++)
         {
            SnmpTrapLogRecord trap = new SnmpTrapLogRecord(msg, varId);
            sendNotification(new SessionNotification(SessionNotification.NEW_SNMP_TRAP, order, trap));
         }
      }

      /**
       * Process syslog messages.
       *
       * @param msg NXCP message
       */
      private void processSyslogRecords(final NXCPMessage msg)
      {
         int count = msg.getFieldAsInt32(NXCPCodes.VID_NUM_RECORDS);
         int order = msg.getFieldAsInt32(NXCPCodes.VID_RECORDS_ORDER);
         long varId = NXCPCodes.VID_SYSLOG_MSG_BASE;
         for(int i = 0; i < count; i++)
         {
            SyslogRecord record = new SyslogRecord(msg, varId);
            sendNotification(new SessionNotification(SessionNotification.NEW_SYSLOG_RECORD, order, record));
         }
      }
      
      /**
       * Process CMD_BULK_ALARM_STATE_CHANGE notification message
       * 
       * @param msg NXCP message
       */
      private void processBulkAlarmStateChange(final NXCPMessage msg)
      {
         int code = msg.getFieldAsInt32(NXCPCodes.VID_NOTIFICATION_CODE) + SessionNotification.NOTIFY_BASE;
         sendNotification(new SessionNotification(code, new BulkAlarmStateChangeData(msg)));
      }

      /**
       * Process CMD_NOTIFY message
       *
       * @param msg NXCP message
       */
      private void processNotificationMessage(final NXCPMessage msg, boolean shiftCode)
      {
         int code = msg.getFieldAsInt32(NXCPCodes.VID_NOTIFICATION_CODE);
         if (shiftCode)
            code += SessionNotification.NOTIFY_BASE;
         long data = msg.getFieldAsInt64(NXCPCodes.VID_NOTIFICATION_DATA);
         
         switch(code)
         {
            case SessionNotification.ALARM_STATUS_FLOW_CHANGED:
               strictAlarmStatusFlow = ((int)data != 0);
               break;
            case SessionNotification.RELOAD_EVENT_DB:
               if (eventTemplatesNeedSync)
               {
                  resyncEventTemplates();
               }
               break;
            case SessionNotification.SESSION_KILLED:
            case SessionNotification.SERVER_SHUTDOWN:
            case SessionNotification.CONNECTION_BROKEN:
               backgroundDisconnect(code);
               return;  // backgroundDisconnect will send disconnect notification
         }
         
         sendNotification(new SessionNotification(code, data));
      }
      
      /**
       * Process CMD_FILE_MONITORING message
       *
       * @param msg NXCP message
       */
      private void processFileTail(final NXCPMessage msg)
      {
         String fileName = msg.getFieldAsString(NXCPCodes.VID_FILE_NAME);
         String fileContent = msg.getFieldAsString(NXCPCodes.VID_FILE_DATA);
         
         synchronized(receivedFileUpdates)
         {
            receivedFileUpdates.put(fileName, fileContent);
            receivedFileUpdates.notifyAll();
         }
      }

      /**
       * Process file data
       *
       * @param msg
       */
      private void processFileData(final NXCPMessage msg)
      {
         long id = msg.getMessageId();
         NXCReceivedFile file;
         synchronized(receivedFiles)
         {
            file = receivedFiles.get(id);
            if (file == null)
            {
               file = new NXCReceivedFile(id);
               receivedFiles.put(id, file);
            }
         }
         int bytes = file.writeData(msg.getBinaryData(), msg.isCompressedStream());
         notifyProgressListener(id, bytes);
         if (msg.isEndOfFile())
         {
            file.close();
            synchronized(receivedFiles)
            {
               receivedFiles.notifyAll();
            }
         }
      }

      /**
       * Process file transfer error
       *
       * @param msg
       */
      private void processFileTransferError(final NXCPMessage msg)
      {
         long id = msg.getMessageId();
         
         NXCReceivedFile file;
         synchronized(receivedFiles)
         {
            file = receivedFiles.get(id);
            if (file == null)
            {
               file = new NXCReceivedFile(id);
               receivedFiles.put(id, file);
            }
            file.abortTransfer(msg.getFieldAsBoolean(NXCPCodes.VID_JOB_CANCELED));
            receivedFiles.notifyAll();
         }
      }

      /**
       * Process updates in user database
       *
       * @param msg Notification message
       */
      private void processUserDBUpdate(final NXCPMessage msg)
      {
         final int code = msg.getFieldAsInt32(NXCPCodes.VID_UPDATE_TYPE);
         final long id = msg.getFieldAsInt64(NXCPCodes.VID_USER_ID);

         AbstractUserObject object = null;
         switch(code)
         {
            case SessionNotification.USER_DB_OBJECT_CREATED:
            case SessionNotification.USER_DB_OBJECT_MODIFIED:
               object = ((id & 0x80000000) != 0) ? new UserGroup(msg) : new User(msg);
               synchronized(userDB)
               {
                  userDB.put(id, object);
               }
               break;
            case SessionNotification.USER_DB_OBJECT_DELETED:
               synchronized(userDB)
               {
                  object = userDB.get(id);
                  if (object != null)
                  {
                     userDB.remove(id);
                  }
               }
               break;
         }

         // Send notification if changed object was found in local database copy
         // or added to it and notification code was known
         if (object != null) 
            sendNotification(new SessionNotification(SessionNotification.USER_DB_CHANGED, code, object));
      }

      /**
       * Process server notification on SNMP trap configuration change
       *
       * @param msg notification message
       */
      private void processTrapConfigChange(final NXCPMessage msg)
      {
         int code = msg.getFieldAsInt32(NXCPCodes.VID_NOTIFICATION_CODE) + SessionNotification.NOTIFY_BASE;
         long id = msg.getFieldAsInt64(NXCPCodes.VID_TRAP_ID);
         SnmpTrap trap = (code != SessionNotification.TRAP_CONFIGURATION_DELETED) ? new SnmpTrap(msg) : null;
         sendNotification(new SessionNotification(code, id, trap));
      }

      /**
       * Process server notification on action configuration change
       *
       * @param msg notification message
       */
      private void processActionConfigChange(final NXCPMessage msg)
      {
         int code = msg.getFieldAsInt32(NXCPCodes.VID_NOTIFICATION_CODE) + SessionNotification.NOTIFY_BASE;
         long id = msg.getFieldAsInt64(NXCPCodes.VID_ACTION_ID);
         ServerAction action = (code != SessionNotification.ACTION_DELETED) ? new ServerAction(msg) : null;
         sendNotification(new SessionNotification(code, id, action));
      }

      /**
       * Process server notification on event configuration change
       *
       * @param msg notification message
       */
      private void processEventConfigChange(final NXCPMessage msg)
      {
         int code = msg.getFieldAsInt32(NXCPCodes.VID_NOTIFICATION_CODE) + SessionNotification.NOTIFY_BASE;
         long eventCode = msg.getFieldAsInt64(NXCPCodes.VID_EVENT_CODE);
         EventTemplate et = (code != SessionNotification.EVENT_TEMPLATE_DELETED) ? new EventTemplate(msg) : null;
         if (eventTemplatesNeedSync)
         {
            synchronized(eventTemplates)
            {
               if (code == SessionNotification.EVENT_TEMPLATE_DELETED)
               {
                  eventTemplates.remove(eventCode);
               }
               else
               {
                  eventTemplates.put(eventCode, et);
               }
            }
         }
         sendNotification(new SessionNotification(code, eventCode, et));
      }
      
      /**
       * Process server notification on image library change
       *
       * @param msg notification message
       */
      public void processImageLibraryUpdate(NXCPMessage msg)
      {
         final UUID imageGuid = msg.getFieldAsUUID(NXCPCodes.VID_GUID);
         final int flags = msg.getFieldAsInt32(NXCPCodes.VID_FLAGS);
         sendNotification(new SessionNotification(SessionNotification.IMAGE_LIBRARY_CHANGED,
            flags == 0 ? SessionNotification.IMAGE_UPDATED : SessionNotification.IMAGE_DELETED, imageGuid));
      }
      
      /**
       * Process server notification on alarm category configuration change
       *
       * @param msg notification message
       */
      private void processAlarmCategoryConfigChange(final NXCPMessage msg)
      {
         int code = msg.getFieldAsInt32(NXCPCodes.VID_NOTIFICATION_CODE) + SessionNotification.NOTIFY_BASE;
         long categoryId = msg.getFieldAsInt64(NXCPCodes.VID_ELEMENT_LIST_BASE);
         AlarmCategory ac = (code != SessionNotification.ALARM_CATEGORY_DELETED) ? new AlarmCategory(msg, NXCPCodes.VID_ELEMENT_LIST_BASE) : null;
         if (alarmCategoriesNeedSync)
         {
            synchronized(alarmCategories)
            {
               if (code == SessionNotification.ALARM_CATEGORY_DELETED)
               {
                  alarmCategories.remove(categoryId);
               }
               else
               {
                  alarmCategories.put(categoryId, ac);
               }
            }
         }
         sendNotification(new SessionNotification(code, categoryId, ac));
      }
   }

   /**
    * Housekeeper thread - cleans received files, etc.
    *
    * @author Victor
    */
   private class HousekeeperThread extends Thread
   {
      private boolean stopFlag = false;

      HousekeeperThread()
      {
         setDaemon(true);
         start();
      }

      /*
       * (non-Javadoc)
       * 
       * @see java.lang.Thread#run()
       */
      @Override
      public void run()
      {
         while(!stopFlag)
         {
            try
            {
               sleep(1000);
            }
            catch(InterruptedException e)
            {
            }

            long currTime = System.currentTimeMillis();

            // Check for old entries in received files
            synchronized(receivedFiles)
            {
               Iterator it = receivedFiles.values().iterator();
               while(it.hasNext())
               {
                  NXCReceivedFile file = it.next();
                  if (file.getTimestamp() + RECEIVED_FILE_TTL < currTime)
                  {
                     file.getFile().delete();
                     it.remove();
                  }
               }
            }
            
            // Check timeouts on message subscriptions
            synchronized(messageSubscriptions)
            {
               Iterator> it = messageSubscriptions.entrySet().iterator();
               while(it.hasNext())
               {
                  Entry e = it.next();
                  MessageHandler h = e.getValue();
                  if (currTime - h.getLastMessageTimestamp() > h.getMessageWaitTimeout())
                  {
                     h.setTimeout();
                     h.setComplete();
                     it.remove();
                  }
               }
            }
         }
      }

      /**
       * @param stopFlag the stopFlag to set
       */
      public void setStopFlag(boolean stopFlag)
      {
         this.stopFlag = stopFlag;
      }
   }
   
   /**
    * Notification processor
    */
   private class NotificationProcessor extends Thread
   {
      private SessionListener[] cachedListenerList = new SessionListener[0];
      
      NotificationProcessor()
      {
         setName("Session Notification Processor");
         setDaemon(true);
         start();
      }

      /* (non-Javadoc)
       * @see java.lang.Thread#run()
       */
      @Override
      public void run()
      {
         while(true)
         {
            SessionNotification n;
            try
            {
               n = notificationQueue.take();
            }
            catch(InterruptedException e)
            {
               continue;
            }
            
            if (n.getCode() == SessionNotification.STOP_PROCESSING_THREAD)
               break;
            
            if (n.getCode() == SessionNotification.UPDATE_LISTENER_LIST)
            {
               synchronized(listeners)
               {
                  cachedListenerList = listeners.toArray(new SessionListener[listeners.size()]);
               }
               continue;
            }

            // loop must be on listeners set copy to prevent 
            // possible deadlock when one of the listeners calls 
            // syncExec on UI thread while UI thread trying to add
            // new listener and stays locked inside addListener
            for(SessionListener l : cachedListenerList)
            {
               try
               {
                  l.notificationHandler(n);
               }
               catch(Exception e)
               {
                  Logger.error("NXCSession.NotificationProcessor", "Unhandled exception in notification handler", e);
               }
            }
         }
         cachedListenerList = null;
      }
   }

   public NXCSession(String connAddress)
   {
      this(connAddress, DEFAULT_CONN_PORT, false);
   }

   public NXCSession(String connAddress, int connPort)
   {
      this(connAddress, connPort, false);
   }

   public NXCSession(String connAddress, int connPort, boolean connUseEncryption)
   {
      this.connAddress = connAddress;
      this.connPort = connPort;
      this.connUseEncryption = connUseEncryption;
   }

   /* (non-Javadoc)
    * @see java.lang.Object#finalize()
    */
   @Override
   protected void finalize() throws Throwable
   {
      try
      {
         disconnect(SessionNotification.CONNECTION_BROKEN);
      }
      finally
      {
         super.finalize();
      }
   }

   /**
    * Create custom object from NXCP message. May be overridden by derived classes to create custom
    * NetXMS objects. This method called before standard object creation, so it can be used for
    * overriding standard object classes. If this method returns null, standard object
    * creation mechanism will be used. Default implementation will always return null.
    *
    * @param objectClass NetXMS object class ID
    * @param msg         Source NXCP message
    * @return NetXMS object or null if object cannot be created
    */
   protected AbstractObject createCustomObjectFromMessage(int objectClass, NXCPMessage msg)
   {
      return null;
   }

   /**
    * Create object from message
    *
    * @param msg Source NXCP message
    * @return NetXMS object
    */
   private AbstractObject createObjectFromMessage(NXCPMessage msg)
   {
      final int objectClass = msg.getFieldAsInt32(NXCPCodes.VID_OBJECT_CLASS);

      AbstractObject object = createCustomObjectFromMessage(objectClass, msg);
      if (object != null) 
         return object;

      switch(objectClass)
      {
         case AbstractObject.OBJECT_ACCESSPOINT:
            object = new AccessPoint(msg, this);
            break;
         case AbstractObject.OBJECT_AGENTPOLICY:
            object = new AgentPolicy(msg, this);
            break;
         case AbstractObject.OBJECT_AGENTPOLICY_CONFIG:
            object = new AgentPolicyConfig(msg, this);
            break;
         case AbstractObject.OBJECT_AGENTPOLICY_LOGPARSER:
            object = new AgentPolicyLogParser(msg, this);
            break;
         case AbstractObject.OBJECT_BUSINESSSERVICE:
            object = new BusinessService(msg, this);
            break;
         case AbstractObject.OBJECT_BUSINESSSERVICEROOT:
            object = new BusinessServiceRoot(msg, this);
            break;
         case AbstractObject.OBJECT_CHASSIS:
            object = new Chassis(msg, this);
            break;
         case AbstractObject.OBJECT_CLUSTER:
            object = new Cluster(msg, this);
            break;
         case AbstractObject.OBJECT_CONDITION:
            object = new Condition(msg, this);
            break;
         case AbstractObject.OBJECT_CONTAINER:
            object = new Container(msg, this);
            break;
         case AbstractObject.OBJECT_DASHBOARDGROUP:
            object = new DashboardGroup(msg, this);
            break;
         case AbstractObject.OBJECT_DASHBOARD:
            object = new Dashboard(msg, this);
            break;
         case AbstractObject.OBJECT_DASHBOARDROOT:
            object = new DashboardRoot(msg, this);
            break;
         case AbstractObject.OBJECT_INTERFACE:
            object = new Interface(msg, this);
            break;
         case AbstractObject.OBJECT_MOBILEDEVICE:
            object = new MobileDevice(msg, this);
            break;
         case AbstractObject.OBJECT_NETWORK:
            object = new EntireNetwork(msg, this);
            break;
         case AbstractObject.OBJECT_NETWORKMAP:
            object = new NetworkMap(msg, this);
            break;
         case AbstractObject.OBJECT_NETWORKMAPGROUP:
            object = new NetworkMapGroup(msg, this);
            break;
         case AbstractObject.OBJECT_NETWORKMAPROOT:
            object = new NetworkMapRoot(msg, this);
            break;
         case AbstractObject.OBJECT_NETWORKSERVICE:
            object = new NetworkService(msg, this);
            break;
         case AbstractObject.OBJECT_NODE:
            object = new Node(msg, this);
            break;
         case AbstractObject.OBJECT_NODELINK:
            object = new NodeLink(msg, this);
            break;
         case AbstractObject.OBJECT_POLICYGROUP:
            object = new PolicyGroup(msg, this);
            break;
         case AbstractObject.OBJECT_POLICYROOT:
            object = new PolicyRoot(msg, this);
            break;
         case AbstractObject.OBJECT_RACK:
            object = new Rack(msg, this);
            break;
         case AbstractObject.OBJECT_SERVICEROOT:
            object = new ServiceRoot(msg, this);
            break;
         case AbstractObject.OBJECT_SLMCHECK:
            object = new ServiceCheck(msg, this);
            break;
         case AbstractObject.OBJECT_SUBNET:
            object = new Subnet(msg, this);
            break;
         case AbstractObject.OBJECT_TEMPLATE:
            object = new Template(msg, this);
            break;
         case AbstractObject.OBJECT_TEMPLATEGROUP:
            object = new TemplateGroup(msg, this);
            break;
         case AbstractObject.OBJECT_TEMPLATEROOT:
            object = new TemplateRoot(msg, this);
            break;
         case AbstractObject.OBJECT_VPNCONNECTOR:
            object = new VPNConnector(msg, this);
            break;
         case AbstractObject.OBJECT_ZONE:
            object = new Zone(msg, this);
            break;
         default:
            object = new GenericObject(msg, this);
            break;
      }

      return object;
   }

   /**
    * Setup encryption
    *
    * @param msg CMD_REQUEST_SESSION_KEY message
    * @throws IOException
    * @throws NXCException
    */
   private void setupEncryption(NXCPMessage msg) throws IOException, NXCException
   {
      final NXCPMessage response = new NXCPMessage(NXCPCodes.CMD_SESSION_KEY, msg.getMessageId());
      response.setEncryptionDisabled(true);

      try
      {
         encryptionContext = EncryptionContext.createInstance(msg);
         response.setField(NXCPCodes.VID_SESSION_KEY, encryptionContext.getEncryptedSessionKey(msg));
         response.setField(NXCPCodes.VID_SESSION_IV, encryptionContext.getEncryptedIv(msg));
         response.setFieldInt16(NXCPCodes.VID_CIPHER, encryptionContext.getCipher());
         response.setFieldInt16(NXCPCodes.VID_KEY_LENGTH, encryptionContext.getKeyLength());
         response.setFieldInt16(NXCPCodes.VID_IV_LENGTH, encryptionContext.getIvLength());
         response.setFieldInt32(NXCPCodes.VID_RCC, RCC.SUCCESS);
         Logger.debug("NXCSession.setupEncryption", "Cipher selected: " + EncryptionContext.getCipherName(encryptionContext.getCipher()));
      }
      catch(Exception e)
      {
         response.setFieldInt32(NXCPCodes.VID_RCC, RCC.NO_CIPHERS);
      }

      sendMessage(response);
   }
   
   /**
    * Wait for synchronization completion
    */
   private void waitForSync(final Semaphore syncObject, final int timeout) throws NXCException
   {
      if (timeout == 0)
      {
         syncObject.acquireUninterruptibly();
      }
      else
      {
         long actualTimeout = timeout;
         boolean success = false;

         while(actualTimeout > 0)
         {
            long startTime = System.currentTimeMillis();
            try
            {
               if (syncObjects.tryAcquire(actualTimeout, TimeUnit.MILLISECONDS))
               {
                  success = true;
                  syncObjects.release();
                  break;
               }
            }
            catch(InterruptedException e)
            {
            }
            actualTimeout -= System.currentTimeMillis() - startTime;
         }
         if (!success) throw new NXCException(RCC.TIMEOUT);
      }
   }

   /**
    * Report synchronization completion
    *
    * @param syncObject Synchronization object
    */
   private void completeSync(final Semaphore syncObject)
   {
      syncObject.release();
   }

   /**
    * Add notification listener
    * 
    * @param listener Listener to add
    */
   public void addListener(SessionListener listener)
   {
      boolean changed;
      synchronized(listeners)
      {
         changed = listeners.add(listener);
      }
      if (changed)
         notificationQueue.offer(new SessionNotification(SessionNotification.UPDATE_LISTENER_LIST));
   }

   /**
    * Remove notification listener
    * 
    * @param listener Listener to remove
    */
   public void removeListener(SessionListener listener)
   {
      boolean changed;
      synchronized(listeners)
      {
         changed = listeners.remove(listener);
      }
      if (changed)
         notificationQueue.offer(new SessionNotification(SessionNotification.UPDATE_LISTENER_LIST));
   }

   /**
    * Add server console listener
    *
    * @param listener The ServerConsoleListener to add
    */
   public void addConsoleListener(ServerConsoleListener listener)
   {
      synchronized(consoleListeners)
      {
         consoleListeners.add(listener);
      }
   }

   /**
    * Remove server console listener
    *
    * @param listener The ServerConsoleListener to remove
    */
   public void removeConsoleListener(ServerConsoleListener listener)
   {
      synchronized(consoleListeners)
      {
         consoleListeners.remove(listener);
      }
   }
   
   /**
    * Subscribe to specific messages
    * 
    * @param messageCode The message code
    * @param messageId  The message ID
    * @param handler The message handler
    */
   public void addMessageSubscription(int messageCode, long messageId, MessageHandler handler)
   {
      synchronized(messageSubscriptions)
      {
         messageSubscriptions.put(new MessageSubscription(messageCode, messageId), handler);
      }
   }
   
   /**
    * Remove message subscription
    * 
    * @param messageCode The message code
    * @param messageId The message ID
    */
   public void removeMessageSubscription(int messageCode, long messageId)
   {
      synchronized(messageSubscriptions)
      {
         messageSubscriptions.remove(new MessageSubscription(messageCode, messageId));
      }
   }
   
   /**
    * Call notification handlers on all registered listeners
    *
    * @param n Notification object
    */
   protected void sendNotification(SessionNotification n)
   {
      if (!notificationQueue.offer(n))
      {
         Logger.debug("NXCSession.sendNotification", "Notification processing queue is full");
      }
   }

   /**
    * Send message to server
    *
    * @param msg Message to sent
    * @throws IOException  in case of socket communication failure
    * @throws NXCException in case of encryption error
    */
   public synchronized void sendMessage(final NXCPMessage msg) throws IOException, NXCException
   {
      if (socket == null)
      {
         throw new IllegalStateException("Not connected to the server. Did you forgot to call connect() first?");
      }
      final OutputStream outputStream = socket.getOutputStream();
      byte[] message;
      if ((encryptionContext != null) && !msg.isEncryptionDisabled())
      {
         try
         {
            message = encryptionContext.encryptMessage(msg, allowCompression);
         }
         catch(GeneralSecurityException e)
         {
            throw new NXCException(RCC.ENCRYPTION_ERROR);
         }
      }
      else
      {
         message = msg.createNXCPMessage(allowCompression);
      }
      outputStream.write(message);
   }

   /**
    * Send file over NXCP
    *
    * @param requestId request ID
    * @param file source file to be sent
    * @param listener progress listener
    * @param allowStreamCompression true if data stream compression is allowed
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   protected void sendFile(final long requestId, final File file, ProgressListener listener, boolean allowStreamCompression) throws IOException, NXCException
   {
      if (listener != null) 
         listener.setTotalWorkAmount(file.length());
      final InputStream inputStream = new BufferedInputStream(new FileInputStream(file));
      sendFileStream(requestId, inputStream, listener, allowStreamCompression);
      inputStream.close();
   }

   /**
    * Send block of data as binary message
    *
    * @param requestId request ID
    * @param data file data
    * @param listener progress listener
    * @param allowStreamCompression true if data stream compression is allowed
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   protected void sendFile(final long requestId, final byte[] data, ProgressListener listener, boolean allowStreamCompression) throws IOException, NXCException
   {
      if (listener != null) 
         listener.setTotalWorkAmount(data.length);
      final InputStream inputStream = new ByteArrayInputStream(data);
      sendFileStream(requestId, inputStream, listener, allowStreamCompression);
      inputStream.close();
   }

   /**
    * Send binary message, data loaded from provided input stream and splitted
    * into chunks of {@value FILE_BUFFER_SIZE} bytes
    *
    * @param requestId request ID
    * @param inputStream data input stream
    * @param listener progress listener
    * @param allowStreamCompression true if data stream compression is allowed
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   private void sendFileStream(final long requestId, final InputStream inputStream, ProgressListener listener, boolean allowStreamCompression) throws IOException, NXCException
   {
      NXCPMessage msg = new NXCPMessage(NXCPCodes.CMD_FILE_DATA, requestId);
      msg.setBinaryMessage(true);
      
      Deflater compressor = allowStreamCompression ? new Deflater(9) : null;
      msg.setStream(true, allowStreamCompression);

      boolean success = false;
      final byte[] buffer = new byte[FILE_BUFFER_SIZE];
      long bytesSent = 0;
      while(true)
      {         
         final int bytesRead = inputStream.read(buffer);
         if (bytesRead < FILE_BUFFER_SIZE)
         {
            msg.setEndOfFile(true);
         }

         if ((compressor != null) && (bytesRead > 0))
         {
            byte[] compressedData = new byte[compressor.deflateBound(bytesRead) + 4];
            compressor.setInput(buffer, 0, bytesRead, false);
            compressor.setOutput(compressedData, 4, compressedData.length - 4);
            if (compressor.deflate(JZlib.Z_SYNC_FLUSH) != JZlib.Z_OK)
               throw new NXCException(RCC.IO_ERROR);
            int length = compressedData.length - compressor.getAvailOut();
            byte[] payload = Arrays.copyOf(compressedData, length);
            payload[0] = 2;   // DEFLATE method
            payload[1] = 0;   // reserved
            payload[2] = (byte)((bytesRead >> 8) & 0xFF);   // uncompressed length, high bits
            payload[3] = (byte)(bytesRead & 0xFF);   // uncompressed length, low bits
            msg.setBinaryData(payload);
         }
         else
         {
            msg.setBinaryData((bytesRead == -1) ? new byte[0] : Arrays.copyOf(buffer, bytesRead));
         }
         sendMessage(msg);

         bytesSent += (bytesRead == -1) ? 0 : bytesRead;
         if (listener != null) 
            listener.markProgress(bytesSent);

         if (bytesRead < FILE_BUFFER_SIZE)
         {
            success = true;
            break;
         }
      }
      if (compressor != null)
         compressor.deflateEnd();

      if (!success)
      {
         NXCPMessage abortMessage = new NXCPMessage(NXCPCodes.CMD_ABORT_FILE_TRANSFER, requestId);
         abortMessage.setBinaryMessage(true);
         sendMessage(abortMessage);
         waitForRCC(abortMessage.getMessageId());
      }
   }

   /**
    * Wait for message with specific code and id.
    * 
    * @param code
    *           Message code
    * @param id
    *           Message id
    * @param timeout
    *           Wait timeout in milliseconds
    * @return Message object
    * @throws NXCException
    *            if message was not arrived within timeout interval
    */
   public NXCPMessage waitForMessage(final int code, final long id, final int timeout) throws NXCException
   {
      final NXCPMessage msg = msgWaitQueue.waitForMessage(code, id, timeout);
      if (msg == null) 
         throw new NXCException(RCC.TIMEOUT);
      return msg;
   }

   /**
    * Wait for message with specific code and id.
    * 
    * @param code
    *           Message code
    * @param id
    *           Message id
    * @return Message object
    * @throws NXCException
    *            if message was not arrived within timeout interval
    */
   public NXCPMessage waitForMessage(final int code, final long id) throws NXCException
   {
      final NXCPMessage msg = msgWaitQueue.waitForMessage(code, id);
      if (msg == null) 
         throw new NXCException(RCC.TIMEOUT);
      return msg;
   }

   /**
    * Wait for CMD_REQUEST_COMPLETED message with given id using default timeout
    * 
    * @param id
    *           Message id
    * @return received message
    * @throws NXCException
    *            if message was not arrived within timeout interval or contains RCC other than RCC.SUCCESS
    */
   public NXCPMessage waitForRCC(final long id) throws NXCException
   {
      return waitForRCC(id, msgWaitQueue.getDefaultTimeout());
   }

   /**
    * Wait for CMD_REQUEST_COMPLETED message with given id
    * 
    * @param id
    *           Message id
    * @param timeout Timeout in milliseconds
    * @return received message
    * @throws NXCException
    *            if message was not arrived within timeout interval or contains RCC other than RCC.SUCCESS
    */
   public NXCPMessage waitForRCC(final long id, final int timeout) throws NXCException
   {
      final NXCPMessage msg = waitForMessage(NXCPCodes.CMD_REQUEST_COMPLETED, id, timeout);
      final int rcc = msg.getFieldAsInt32(NXCPCodes.VID_RCC);
      if (rcc != RCC.SUCCESS)
      {
         if ((rcc == RCC.COMPONENT_LOCKED) && (msg.findField(NXCPCodes.VID_LOCKED_BY) != null))
         {
            throw new NXCException(rcc, msg.getFieldAsString(NXCPCodes.VID_LOCKED_BY));
         }
         else if (msg.findField(NXCPCodes.VID_ERROR_TEXT) != null)
         {
            throw new NXCException(rcc, msg.getFieldAsString(NXCPCodes.VID_ERROR_TEXT));
         }
         else if (msg.findField(NXCPCodes.VID_VALUE) != null)
         {
            throw new NXCException(rcc, msg.getFieldAsString(NXCPCodes.VID_VALUE));
         }
         else
         {
            throw new NXCException(rcc);
         }
      }
      return msg;
   }

   /**
    * Create new NXCP message with unique id
    * 
    * @param code
    *           Message code
    * @return New message object
    */
   public final NXCPMessage newMessage(int code)
   {
      return new NXCPMessage(code, requestId.getAndIncrement());
   }

   /**
    * Wait for specific file to arrive
    *
    * @param id      Message ID
    * @param timeout Wait timeout in milliseconds
    * @return Received file or null in case of failure
    */
   public ReceivedFile waitForFile(final long id, final int timeout)
   {
      int timeRemaining = timeout;
      File file = null;
      int status = ReceivedFile.FAILED;

      while(timeRemaining > 0)
      {
         synchronized(receivedFiles)
         {
            NXCReceivedFile rf = receivedFiles.get(id);
            if (rf != null)
            {
               if (rf.getStatus() != NXCReceivedFile.OPEN)
               {
                  if (rf.getStatus() == NXCReceivedFile.RECEIVED) 
                  {
                     file = rf.getFile();
                     status = ReceivedFile.SUCCESS;
                  }
                  break;
               }
            }

            long startTime = System.currentTimeMillis();
            try
            {
               receivedFiles.wait(timeRemaining);
            }
            catch(InterruptedException e)
            {
            }
            timeRemaining -= System.currentTimeMillis() - startTime;
         }
      }
      return new ReceivedFile(file, timeRemaining <= 0 ? ReceivedFile.TIMEOUT : status);
   }
   
   /**
    * Wait for specific file tail to arrive
    *
    * @param fileName      Waiting file name
    * @param timeout       Wait timeout in milliseconds
    * @return Received tail string or null in case of failure
    */
   public String waitForFileTail(String fileName, final int timeout)
   {
      int timeRemaining = timeout;
      String tail = null;

      while(timeRemaining > 0)
      {
         synchronized(receivedFileUpdates)
         {
            tail = receivedFileUpdates.get(fileName);
            if (tail != null)
            {
               receivedFileUpdates.remove(fileName);
               break;
            }

            long startTime = System.currentTimeMillis();
            try
            {
               receivedFileUpdates.wait(timeRemaining);
            }
            catch(InterruptedException e)
            {
            }
            timeRemaining -= System.currentTimeMillis() - startTime;
         }
      }
      return tail;
   }   

   /**
    * Execute simple commands (without arguments and only returning RCC)
    *
    * @param command Command code
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   protected void executeSimpleCommand(int command) throws IOException, NXCException
   {
      final NXCPMessage msg = newMessage(command);
      sendMessage(msg);
      waitForRCC(msg.getMessageId());
   }

   /**
    * Receive table from server.
    *
    * @param requestId request ID
    * @param msgCode   Message code
    * @return Received table
    * @throws NXCException if operation was timed out
    */
   public Table receiveTable(long requestId, int msgCode) throws NXCException
   {
      NXCPMessage msg = waitForMessage(msgCode, requestId);
      Table table = new Table(msg);
      while(!msg.isEndOfSequence())
      {
         msg = waitForMessage(msgCode, requestId);
         table.addDataFromMessage(msg);
      }
      return table;
   }

   /**
    * Connect to NetMS server. Establish connection with the server and set up encryption if required.
    * Only base protocol version check will be performed. Login must be performed before using session
    * after successful connect.
    * 
    * @throws IOException  if socket I/O error occurs
    * @throws UnknownHostException if the host is unknown
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    * @throws IllegalStateException if the state is illegal
    */
   public void connect() throws IOException, UnknownHostException, NXCException, IllegalStateException
   {
      connect(null);
   }
   
   /**
    * Connect to NetMS server. Establish connection with the server and set up encryption if required.
    * Versions of protocol components given in *componentVersions* will be validated. Login must be 
    * performed before using session after successful connect.
    * 
    * @param componentVersions The versions of the components
    * @throws IOException  if socket I/O error occurs
    * @throws UnknownHostException if the host is unknown
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    * @throws IllegalStateException if the state is illegal
    */
   public void connect(int[] componentVersions) throws IOException, UnknownHostException, NXCException, IllegalStateException
   {
      if (connected)
         throw new IllegalStateException("Session already connected");

      if (disconnected)
         throw new IllegalStateException("Session already disconnected and cannot be reused");
      
      encryptionContext = null;
      Logger.info("NXCSession.connect", "Connecting to " + connAddress + ":" + connPort);
      try
      {
         socket = new Socket();
         socket.connect(new InetSocketAddress(connAddress, connPort), connectTimeout);
         msgWaitQueue = new NXCPMsgWaitQueue(commandTimeout);
         recvThread = new ReceiverThread();
         housekeeperThread = new HousekeeperThread();
         new NotificationProcessor();

         // get server information
         Logger.debug("NXCSession.connect", "connection established, retrieving server info");
         NXCPMessage request = newMessage(NXCPCodes.CMD_GET_SERVER_INFO);
         sendMessage(request);
         NXCPMessage response = waitForMessage(NXCPCodes.CMD_REQUEST_COMPLETED, request.getMessageId());

         protocolVersion = new ProtocolVersion(response);
         if (!ignoreProtocolVersion)
         {
            if (!protocolVersion.isCorrectVersion(ProtocolVersion.INDEX_BASE) ||
                ((componentVersions != null) && !validateProtocolVersions(componentVersions)))
            {
               Logger.warning("NXCSession.connect", "connection failed (" + protocolVersion.toString() + ")");
               throw new NXCException(RCC.BAD_PROTOCOL, protocolVersion.toString());
            }
         }
         else
         {
            Logger.debug("NXCSession.connect", "protocol version ignored");
         }

         serverVersion = response.getFieldAsString(NXCPCodes.VID_SERVER_VERSION);
         serverId = response.getFieldAsInt64(NXCPCodes.VID_SERVER_ID);
         serverTimeZone = response.getFieldAsString(NXCPCodes.VID_TIMEZONE);
         serverTime = response.getFieldAsInt64(NXCPCodes.VID_TIMESTAMP) * 1000;
         serverTimeRecvTime = System.currentTimeMillis();
         serverChallenge = response.getFieldAsBinary(NXCPCodes.VID_CHALLENGE);

         tileServerURL = response.getFieldAsString(NXCPCodes.VID_TILE_SERVER_URL);
         if (tileServerURL != null)
         {
            if (!tileServerURL.endsWith("/")) tileServerURL = tileServerURL.concat("/");
         }
         else
         {
            tileServerURL = "http://tile.openstreetmap.org/";
         }

         dateFormat = response.getFieldAsString(NXCPCodes.VID_DATE_FORMAT);
         if ((dateFormat == null) || (dateFormat.length() == 0)) 
            dateFormat = "dd.MM.yyyy";

         timeFormat = response.getFieldAsString(NXCPCodes.VID_TIME_FORMAT);
         if ((timeFormat == null) || (timeFormat.length() == 0)) 
            timeFormat = "HH:mm:ss";

         shortTimeFormat = response.getFieldAsString(NXCPCodes.VID_SHORT_TIME_FORMAT);
         if ((shortTimeFormat == null) || (shortTimeFormat.length() == 0)) 
            shortTimeFormat = "HH:mm";
         
         int count = response.getFieldAsInt32(NXCPCodes.VID_NUM_COMPONENTS);
         long fieldId = NXCPCodes.VID_COMPONENT_LIST_BASE;
         for(int i = 0; i < count; i++)
            serverComponents.add(response.getFieldAsString(fieldId++));

         // Setup encryption if required
         if (connUseEncryption)
         {
            request = newMessage(NXCPCodes.CMD_REQUEST_ENCRYPTION);
            request.setFieldInt16(NXCPCodes.VID_USE_X509_KEY_FORMAT, 1);
            sendMessage(request);
            waitForRCC(request.getMessageId());
         }

         Logger.debug("NXCSession.connect", "Connected to server version " + serverVersion);
         connected = true;
      }
      finally
      {
         if (!connected) 
            disconnect(SessionNotification.USER_DISCONNECT);
      }
   }
   
   /**
    * Login to server using login name and password.
    * 
    * @param login login name
    * @param password password
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    * @throws IllegalStateException if the state is illegal
    */
   public void login(String login, String password) throws NXCException, IOException, IllegalStateException
   {
      login(AuthenticationType.PASSWORD, login, password, null, null);
   }
   
   /**
    * Login to server using certificate.
    * 
    * @param login login name
    * @param certificate user's certificate
    * @param signature user's digital signature
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    * @throws IllegalStateException if the state is illegal
    */
   public void login(String login, Certificate certificate, Signature signature) throws NXCException, IOException, IllegalStateException
   {
      login(AuthenticationType.CERTIFICATE, login, null, certificate, signature);
   }
   
   /**
    * Login to server.
    * 
    * @param authType authentication type
    * @param login login name
    * @param password password
    * @param certificate user's certificate
    * @param signature user's digital signature
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    * @throws IllegalStateException if the state is illegal
    */
   public void login(AuthenticationType authType, String login, String password, Certificate certificate, Signature signature) throws NXCException, IOException, IllegalStateException
   {
      if (!connected)
         throw new IllegalStateException("Session not connected");

      if (disconnected)
         throw new IllegalStateException("Session already disconnected and cannot be reused");
      
      authenticationMethod = authType;
      userName = login;
      
      final NXCPMessage request = newMessage(NXCPCodes.CMD_LOGIN);
      request.setFieldInt16(NXCPCodes.VID_AUTH_TYPE, authType.getValue());
      request.setField(NXCPCodes.VID_LOGIN_NAME, login);

      if ((authType == AuthenticationType.PASSWORD) || (authType == AuthenticationType.SSO_TICKET))
      {
         request.setField(NXCPCodes.VID_PASSWORD, password);
      }
      else if (authType == AuthenticationType.CERTIFICATE)
      {
         if ((serverChallenge == null) || (signature == null) || (certificate == null))
         {
            throw new NXCException(RCC.ENCRYPTION_ERROR);
         }
         byte[] signedChallenge = signChallenge(signature, serverChallenge);
         request.setField(NXCPCodes.VID_SIGNATURE, signedChallenge);
         try
         {
            request.setField(NXCPCodes.VID_CERTIFICATE, certificate.getEncoded());
         }
         catch(CertificateEncodingException e)
         {
            throw new NXCException(RCC.ENCRYPTION_ERROR);
         }
      }

      request.setField(NXCPCodes.VID_LIBNXCL_VERSION, NXCommon.VERSION);
      request.setField(NXCPCodes.VID_CLIENT_INFO, connClientInfo);
      request.setField(NXCPCodes.VID_OS_INFO, System.getProperty("os.name") + " " + System.getProperty("os.version"));
      request.setFieldInt16(NXCPCodes.VID_CLIENT_TYPE, clientType);
      if (clientAddress != null) 
         request.setField(NXCPCodes.VID_CLIENT_ADDRESS, clientAddress);
      if (clientLanguage != null)
         request.setField(NXCPCodes.VID_LANGUAGE, clientLanguage);
      request.setFieldInt16(NXCPCodes.VID_ENABLE_COMPRESSION, 1);
      sendMessage(request);
      
      final NXCPMessage response = waitForMessage(NXCPCodes.CMD_LOGIN_RESP, request.getMessageId());
      int rcc = response.getFieldAsInt32(NXCPCodes.VID_RCC);
      Logger.debug("NXCSession.login", "CMD_LOGIN_RESP received, RCC=" + rcc);
      if (rcc != RCC.SUCCESS)
      {
         Logger.warning("NXCSession.login", "Login failed, RCC=" + rcc);
         throw new NXCException(rcc);
      }
      userId = response.getFieldAsInt32(NXCPCodes.VID_USER_ID);
      sessionId = response.getFieldAsInt32(NXCPCodes.VID_SESSION_ID);
      userSystemRights = response.getFieldAsInt64(NXCPCodes.VID_USER_SYS_RIGHTS);
      passwordExpired = response.getFieldAsBoolean(NXCPCodes.VID_CHANGE_PASSWD_FLAG);
      graceLogins = response.getFieldAsInt32(NXCPCodes.VID_GRACE_LOGINS);
      zoningEnabled = response.getFieldAsBoolean(NXCPCodes.VID_ZONING_ENABLED);
      helpdeskLinkActive = response.getFieldAsBoolean(NXCPCodes.VID_HELPDESK_LINK_ACTIVE);

      defaultDciPollingInterval = response.getFieldAsInt32(NXCPCodes.VID_POLLING_INTERVAL);
      if (defaultDciPollingInterval == 0) 
         defaultDciPollingInterval = 60;

      defaultDciRetentionTime = response.getFieldAsInt32(NXCPCodes.VID_RETENTION_TIME);
      if (defaultDciRetentionTime == 0) 
         defaultDciRetentionTime = 30;
      
      minViewRefreshInterval = response.getFieldAsInt32(NXCPCodes.VID_VIEW_REFRESH_INTERVAL);
      if (minViewRefreshInterval <= 0)
         minViewRefreshInterval = 200;

      strictAlarmStatusFlow = response.getFieldAsBoolean(NXCPCodes.VID_ALARM_STATUS_FLOW_STATE);
      timedAlarmAckEnabled = response.getFieldAsBoolean(NXCPCodes.VID_TIMED_ALARM_ACK_ENABLED);
      
      serverCommandOutputTimeout = response.getFieldAsInt32(NXCPCodes.VID_SERVER_COMMAND_TIMEOUT) * 1000;
      serverColor = response.getFieldAsString(NXCPCodes.VID_SERVER_COLOR);
      serverName = response.getFieldAsString(NXCPCodes.VID_SERVER_NAME);
      if ((serverName == null) || serverName.isEmpty())
         serverName = connAddress;

      // compatibility with 2.2.x before 2.2.6
      int count = response.getFieldAsInt32(NXCPCodes.VID_ALARM_LIST_DISP_LIMIT);
      clientConfigurationHints.put("AlarmList.DisplayLimit", Integer.toString(count));
      
      count = response.getFieldAsInt32(NXCPCodes.VID_CONFIG_HINT_COUNT);
      if (count > 0)
      {
         long fieldId = NXCPCodes.VID_CONFIG_HINT_LIST_BASE;
         for(int i = 0; i < count; i++)
         {
            String key = response.getFieldAsString(fieldId++);
            String value = response.getFieldAsString(fieldId++);
            clientConfigurationHints.put(key, value);
            Logger.info("NXCSession.login", "configuration hint: " + key + " = " + value);
         }
      }

      messageOfTheDay = response.getFieldAsString(NXCPCodes.VID_MESSAGE_OF_THE_DAY);

      allowCompression = response.getFieldAsBoolean(NXCPCodes.VID_ENABLE_COMPRESSION);
      
      Logger.info("NXCSession.login", "succesfully logged in, userId=" + userId);
   }
   
   /**
    * Disconnect session in background
    * 
    * @param reason disconnect reason (appropriate session notification code)
    */
   private void backgroundDisconnect(final int reason)
   {
      Thread t = new Thread(new Runnable() {
         @Override
         public void run()
         {
            disconnect(reason);
         }
      }, "NXCSession disconnect");
      t.setDaemon(true);
      t.start();
   }

   /**
    * Disconnect from server.
    * 
    * @param reason disconnect reason (appropriate session notification code)
    */
   synchronized private void disconnect(int reason)
   {
      if (disconnected)
         return;
      
      if (socket != null)
      {
         try
         {
            socket.shutdownInput();
            socket.shutdownOutput();
         }
         catch(IOException e)
         {
         }

         try
         {
            socket.close();
         }
         catch(IOException e)
         {
         }
      }
      
      // cause notification processing thread to stop
      notificationQueue.clear();
      if (reason != SessionNotification.USER_DISCONNECT)
         notificationQueue.offer(new SessionNotification(reason));
      notificationQueue.offer(new SessionNotification(SessionNotification.STOP_PROCESSING_THREAD));

      if (recvThread != null)
      {
         while(recvThread.isAlive())
         {
            try
            {
               recvThread.join();
            }
            catch(InterruptedException e)
            {
            }
         }
         recvThread = null;
      }

      if (housekeeperThread != null)
      {
         housekeeperThread.setStopFlag(true);
         while(housekeeperThread.isAlive())
         {
            try
            {
               housekeeperThread.join();
            }
            catch(InterruptedException e)
            {
            }
         }
         housekeeperThread = null;
      }

      if (msgWaitQueue != null)
      {
         msgWaitQueue.shutdown();
         msgWaitQueue = null;
      }

      connected = false;
      disconnected = true;
      socket = null;
      
      listeners.clear();
      consoleListeners.clear();
      messageSubscriptions.clear();
      receivedFiles.clear();
      receivedFileUpdates.clear();
      objectList.clear();
      objectListGUID.clear();
      zoneList.clear();
      eventTemplates.clear();
      userDB.clear();
      alarmCategories.clear();
   }
   
   /**
    * Disconnect from server.
    */
   public void disconnect()
   {
      disconnect(SessionNotification.USER_DISCONNECT);
   }

   /**
    * Get connection state
    * @return connection state
    */
   public boolean isConnected()
   {
      return connected;
   }

   /**
    * Get encryption state for current session.
    * 
    * @return true if session is encrypted
    */
   public boolean isEncrypted()
   {
      return connUseEncryption;
   }

   /**
    * Get the state of protocol version check
    * 
    * @return true if protocol version should not be checked
    */
   public boolean isIgnoreProtocolVersion()
   {
      return ignoreProtocolVersion;
   }

   /**
    * If set to true, protocol version is not checked at connect.
    *
    * @param ignoreProtocolVersion true if protocol version should not be checked
    */
   public void setIgnoreProtocolVersion(boolean ignoreProtocolVersion)
   {
      this.ignoreProtocolVersion = ignoreProtocolVersion;
   }
   
   /**
    * Validate protocol versions
    * 
    * @param versions the protocol versions
    * @return true if protocol versions are valid
    */
   public boolean validateProtocolVersions(int[] versions)
   {
      if (protocolVersion == null)
         return false;
      for(int index : versions)
         if (!protocolVersion.isCorrectVersion(index))
            return false;
      return true;
   }

   /**
    * Get default receiver buffer size.
    * 
    * @return Default receiver buffer size in bytes.
    */
   public int getDefaultRecvBufferSize()
   {
      return defaultRecvBufferSize;
   }

   /**
    * Get max receiver buffer size.
    * 
    * @return Max receiver buffer size in bytes.
    */
   public int getMaxRecvBufferSize()
   {
      return maxRecvBufferSize;
   }

   /**
    * Set receiver buffer size. This method should be called before connect(). It will not have any effect after
    * connect().
    * 
    * @param defaultBufferSize default size of receiver buffer in bytes.
    * @param maxBufferSize max size of receiver buffer in bytes.
    */
   public void setRecvBufferSize(int defaultBufferSize, int maxBufferSize)
   {
      this.defaultRecvBufferSize = defaultBufferSize;
      this.maxRecvBufferSize = maxBufferSize;
   }

   /**
    * Get server address
    * 
    * @return Server address
    */
   public String getServerAddress()
   {
      return connAddress;
   }

   /**
    * Get NetXMS server version.
    * 
    * @return Server version
    */
   public String getServerVersion()
   {
      return serverVersion;
   }

   /**
    * Get NetXMS server UID.
    * 
    * @return Server UID
    */
   public long getServerId()
   {
      return serverId;
   }

   /**
    * Get server time zone.
    * 
    * @return server's time zone string
    */
   public String getServerTimeZone()
   {
      return serverTimeZone;
   }

   /**
    * Get server name
    * 
    * @return the serverName
    */
   public String getServerName()
   {
      return serverName;
   }

   /**
    * Get server identification colour
    * 
    * @return the serverColor
    */
   public String getServerColor()
   {
      return serverColor;
   }

   /**
    * Get server time
    * 
    * @return the serverTime
    */
   public long getServerTime()
   {
      long offset = System.currentTimeMillis() - serverTimeRecvTime;
      return serverTime + offset;
   }
   
   /**
    * @return the serverChallenge
    */
   public byte[] getServerChallenge()
   {
      return serverChallenge;
   }
   
   /**
    * Check if server component with given id is registered
    * 
    * @param componentId The component ID
    * @return true if server component is registered
    */
   public boolean isServerComponentRegistered(String componentId)
   {
      return serverComponents.contains(componentId);
   }
   
   /**
    * Get list of registered server components
    * 
    * @return Array of registered server components
    */
   public String[] getRegisteredServerComponents()
   {
      return serverComponents.toArray(new String[serverComponents.size()]);
   }

   /**
    * Get server URL
    * 
    * @return the tileServerURL
    */
   public String getTileServerURL()
   {
      return tileServerURL;
   }

   /**
    * Get the state of zoning
    * 
    * @return true if zoning is enabled
    */
   public boolean isZoningEnabled()
   {
      return zoningEnabled;
   }

   /**
    * Get status of helpdesk integration module on server.
    * 
    * @return true if helpdesk integration module loaded on server
    */
   public boolean isHelpdeskLinkActive()
   {
      return helpdeskLinkActive;
   }

   /**
    * Get client information string
    * 
    * @return The client information
    */
   public String getClientInfo()
   {
      return connClientInfo;
   }

   /**
    * Set client information string
    * 
    * @param connClientInfo The client info to set
    */
   public void setClientInfo(final String connClientInfo)
   {
      this.connClientInfo = connClientInfo;
   }

   /**
    * Set command execution timeout.
    * 
    * @param commandTimeout
    *           New command timeout
    */
   public void setCommandTimeout(final int commandTimeout)
   {
      this.commandTimeout = commandTimeout;
   }

   /**
    * Set connect call timeout (must be set before connect call)
    * 
    * @param connectTimeout connect timeout in milliseconds
    */
   public void setConnectTimeout(int connectTimeout)
   {
      this.connectTimeout = connectTimeout;
   }

   /**
    * Get identifier of logged in user.
    * 
    * @return Identifier of logged in user
    */
   public int getUserId()
   {
      return userId;
   }

   /**
    * Get the current user name
    * 
    * @return the userName
    */
   public String getUserName()
   {
      return userName;
   }

   /**
    * Get the current authentication method
    * 
    * @return the authenticationMethod
    */
   public AuthenticationType getAuthenticationMethod()
   {
      return authenticationMethod;
   }

   /**
    * Get system-wide rights of currently logged in user.
    * 
    * @return System-wide rights of currently logged in user
    */
   public long getUserSystemRights()
   {
      return userSystemRights;
   }
   
   /** Get message of the day if server config is set
    * @return Message of the day
    */
   public String getMessageOfTheDay()
   {
      return messageOfTheDay;
   }

   /**
    * Check if password is expired for currently logged in user.
    * 
    * @return true if password is expired
    */
   public boolean isPasswordExpired()
   {
      return passwordExpired;
   }
   
   /**
    * Get number of remaining grace logins
    * 
    * @return number of remaining grace logins
    */
   public int getGraceLogins()
   {
      return graceLogins;
   }
   
   /**
    * Get maximum number of records allowed to be displayed in alarm list
    * 
    * @return The limit of alarms displayed
    */
   public int getAlarmListDisplayLimit()
   {
		return getClientConfigurationHintAsInt("AlarmList.DisplayLimit", 4096);
   }
   
   /**
    * Get client configuration hint as string
    * 
    * @param name hint name
    * @param defaultValue default value (returned if given hint was not provided by server)
    * @return hint value as provided by server or default value
    */
   public String getClientConfigurationHint(String name, String defaultValue)
   {
      String v = clientConfigurationHints.get(name);
      return (v != null) ? v : defaultValue;
   }

   /**
    * Get client configuration hint as string
    * 
    * @param name hint name
    * @return hint value as provided by server or null
    */
   public String getClientConfigurationHint(String name)
   {
      return getClientConfigurationHint(name, null);
   }
   
   /**
    * Get client configuration hint as integer
    * 
    * @param name hint name
    * @param defaultValue default value (returned if given hint was not provided by server or is not valid integer)
    * @return hint value as provided by server or default value
    */
   public int getClientConfigurationHintAsInt(String name, int defaultValue)
   {
      String v = clientConfigurationHints.get(name);
      if (v == null)
         return defaultValue;
      try
      {
         return Integer.parseInt(v);
      }
      catch(NumberFormatException e)
      {
         return defaultValue;
      }
   }
   
   /**
    * Get client configuration hint as boolean
    * 
    * @param name hint name
    * @param defaultValue default value (returned if given hint was not provided by server or is not valid boolean)
    * @return hint value as provided by server or default value
    */
   public boolean getClientConfigurationHintAsBoolean(String name, boolean defaultValue)
   {
      String v = clientConfigurationHints.get(name);
      if (v == null)
         return defaultValue;
      if (v.equalsIgnoreCase("true"))
         return true;
      if (v.equalsIgnoreCase("false"))
         return false;
      try
      {
         return Integer.parseInt(v) != 0;
      }
      catch(NumberFormatException e)
      {
         return defaultValue;
      }
   }
   
   /**
    * Synchronizes NetXMS objects between server and client. After successful
    * sync, subscribe client to object change notifications.
    *
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public synchronized void syncObjects() throws IOException, NXCException
   {
      syncObjects.acquireUninterruptibly();
      NXCPMessage msg = newMessage(NXCPCodes.CMD_GET_OBJECTS);
      msg.setFieldInt16(NXCPCodes.VID_SYNC_COMMENTS, 1);
      sendMessage(msg);
      waitForRCC(msg.getMessageId());
      waitForSync(syncObjects, commandTimeout * 10);
      objectsSynchronized = true;
      sendNotification(new SessionNotification(SessionNotification.OBJECT_SYNC_COMPLETED));
      subscribe(CHANNEL_OBJECTS);
   }

   /**
    * Synchronizes selected object set with the server.
    *
    * @param objects      identifiers of objects need to be synchronized
    * @param syncComments if true, comments for objects will be synchronized as well
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public void syncObjectSet(long[] objects, boolean syncComments) throws IOException, NXCException
   {
      syncObjectSet(objects, syncComments, 0);
   }

   /**
    * Synchronizes selected object set with the server. The following options are accepted:
    * OBJECT_SYNC_NOTIFY - send object update notification for each received object
    * OBJECT_SYNC_WAIT   - wait until all requested objects received
    *
    * @param objects      identifiers of objects need to be synchronized
    * @param syncComments if true, comments for objects will be synchronized as well
    * @param options      sync options (see above)
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public void syncObjectSet(long[] objects, boolean syncComments, int options) throws IOException, NXCException
   {
      NXCPMessage msg = newMessage(NXCPCodes.CMD_GET_SELECTED_OBJECTS);
      msg.setFieldInt16(NXCPCodes.VID_SYNC_COMMENTS, syncComments ? 1 : 0);
      msg.setFieldInt16(NXCPCodes.VID_FLAGS, options);
      msg.setFieldInt32(NXCPCodes.VID_NUM_OBJECTS, objects.length);
      msg.setField(NXCPCodes.VID_OBJECT_LIST, objects);
      sendMessage(msg);
      waitForRCC(msg.getMessageId());

      if ((options & OBJECT_SYNC_WAIT) != 0) waitForRCC(msg.getMessageId());
   }

   /**
    * Synchronize only those objects from given set which are not synchronized yet.
    *
    * @param objects      identifiers of objects need to be synchronized
    * @param syncComments if true, comments for objects will be synchronized as well
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public void syncMissingObjects(long[] objects, boolean syncComments) throws IOException, NXCException
   {
      syncMissingObjects(objects, syncComments, 0);
   }

   /**
    * Synchronize only those objects from given set which are not synchronized yet.
    * Accepts all options which are valid for syncObjectSet.
    *
    * @param objects      identifiers of objects need to be synchronized
    * @param syncComments if true, comments for objects will be synchronized as well
    * @param options      sync options (see comments for syncObjectSet)
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public void syncMissingObjects(long[] objects, boolean syncComments, int options) throws IOException, NXCException
   {
      final long[] syncList = Arrays.copyOf(objects, objects.length);
      int count = syncList.length;
      synchronized(objectList)
      {
         for(int i = 0; i < syncList.length; i++)
         {
            if (objectList.containsKey(syncList[i]))
            {
               syncList[i] = 0;
               count--;
            }
         }
      }

      if (count > 0)
      {
         syncObjectSet(syncList, syncComments, options);
      }
   }

   /**
    * Find NetXMS object by it's identifier.
    *
    * @param id Object identifier
    * @return Object with given ID or null if object cannot be found
    */
   public AbstractObject findObjectById(final long id)
   {
      synchronized(objectList)
      {
         return objectList.get(id);
      }
   }

   /**
    * Find NetXMS object by it's identifier with additional class checking.
    *
    * @param id            object identifier
    * @param requiredClass required object class
    * @param  Object
    * @return Object with given ID or null if object cannot be found or is not an instance of required class
    */
   @SuppressWarnings("unchecked")
   public  T findObjectById(final long id, final Class requiredClass)
   {
      AbstractObject object = findObjectById(id);
      return requiredClass.isInstance(object) ? (T)object : null;
   }

   /**
    * Find multiple NetXMS objects by identifiers
    *
    * @param idList        array of object identifiers
    * @param returnUnknown if true, this method will return UnknownObject placeholders for unknown object identifiers
    * @return list of found objects
    */
   public List findMultipleObjects(final long[] idList, boolean returnUnknown)
   {
      return findMultipleObjects(idList, null, returnUnknown);
   }

   /**
    * Find multiple NetXMS objects by identifiers
    *
    * @param idList        array of object identifiers
    * @param classFilter   class filter for objects, or null to disable filtering
    * @param returnUnknown if true, this method will return UnknownObject placeholders for unknown object identifiers
    * @return list of found objects
    */
   public List findMultipleObjects(final long[] idList, Class classFilter, boolean returnUnknown)
   {
      List result = new ArrayList(idList.length);

      synchronized(objectList)
      {
         for(int i = 0; i < idList.length; i++)
         {
            final AbstractObject object = objectList.get(idList[i]);
            if ((object != null) && ((classFilter == null) || classFilter.isInstance(object)))
            {
               result.add(object);
            }
            else if (returnUnknown)
            {
               result.add(new UnknownObject(idList[i], this));
            }
         }
      }

      return result;
   }

   /**
    * Find multiple NetXMS objects by identifiers
    *
    * @param idList        array of object identifiers
    * @param returnUnknown if true, this method will return UnknownObject placeholders for unknown object identifiers
    * @return array of found objects
    */
   public List findMultipleObjects(final Long[] idList, boolean returnUnknown)
   {
      return findMultipleObjects(idList, null, returnUnknown);
   }

   /**
    * Find multiple NetXMS objects by identifiers
    *
    * @param idList        array of object identifiers
    * @param classFilter   class filter for objects, or null to disable filtering
    * @param returnUnknown if true, this method will return UnknownObject placeholders for unknown object identifiers
    * @return array of found objects
    */
   public List findMultipleObjects(final Long[] idList, Class classFilter, boolean returnUnknown)
   {
      List result = new ArrayList(idList.length);

      synchronized(objectList)
      {
         for(int i = 0; i < idList.length; i++)
         {
            final AbstractObject object = objectList.get(idList[i]);
            if ((object != null) && ((classFilter == null) || classFilter.isInstance(object)))
            {
               result.add(object);
            }
            else if (returnUnknown)
            {
               result.add(new UnknownObject(idList[i], this));
            }
         }
      }

      return result;
   }

   /**
    * Find NetXMS object by it's GUID.
    *
    * @param guid Object GUID
    * @return Object with given ID or null if object cannot be found
    */
   public AbstractObject findObjectByGUID(final UUID guid)
   {
      synchronized(objectList)
      {
         return objectListGUID.get(guid);
      }
   }

   /**
    * Find NetXMS object by it's GUID with additional class checking.
    *
    * @param guid            object GUID
    * @param requiredClass required object class
    * @param  Object
    * @return Object with given ID or null if object cannot be found or is not an instance of required class
    */
   @SuppressWarnings("unchecked")
   public  T findObjectByGUID(final UUID guid, final Class requiredClass)
   {
      AbstractObject object = findObjectByGUID(guid);
      return requiredClass.isInstance(object) ? (T)object : null;
   }

   /**
    * Find zone object by zone UIN (unique identification number).
    *
    * @param zoneUIN zone UIN to find
    * @return zone object or null
    */
   public Zone findZone(long zoneUIN)
   {
      synchronized(objectList)
      {
         return zoneList.get(zoneUIN);
      }
   }
   
   /**
    * Get all accessible zone objects
    * 
    * @return list of all accessible zone objects
    */
   public List getAllZones()
   {
      synchronized(objectList)
      {
         return new ArrayList(zoneList.values());
      }
   }
   
   /**
    * Find object by name. If multiple objects with same name exist,
    * it is not determined what object will be returned. Name comparison
    * is case-insensitive.
    *
    * @param name object name to find
    * @return object with matching name or null
    */
   public AbstractObject findObjectByName(final String name)
   {
      AbstractObject result = null;
      synchronized(objectList)
      {
         for(AbstractObject object : objectList.values())
         {
            if (object.getObjectName().equalsIgnoreCase(name))
            {
               result = object;
               break;
            }
         }
      }
      return result;
   }

   /**
    * Find object by name using regular expression. If multiple objects with same name exist,
    * it is not determined what object will be returned. Name comparison is case-insensitive.
    *
    * @param pattern regular expression for matching object name
    * @return object with matching name or null
    */
   public AbstractObject findObjectByNamePattern(final String pattern)
   {
      AbstractObject result = null;
      Matcher matcher = Pattern.compile(pattern).matcher("");
      synchronized(objectList)
      {
         for(AbstractObject object : objectList.values())
         {
            matcher.reset(object.getObjectName());
            if (matcher.matches())
            {
               result = object;
               break;
            }
         }
      }
      return result;
   }
   
   /**
    * Generic object find using filter. WIll return first object matching given filter.
    * 
    * @param filter ObjectFilter to filter the result
    * @return first matching object or null
    */
   public AbstractObject findObject(ObjectFilter filter)
   {
      AbstractObject result = null;
      synchronized(objectList)
      {
         for(AbstractObject object : objectList.values())
         {
            if (filter.filter(object))
            {
               result = object;
               break;
            }
         }
      }
      return result;
   }

   /**
    * Find all objects matching given filter.
    * 
    * @param filter ObjectFilter to filter the result
    * @return list of matching objects (empty list if nothing found)
    */
   public List filterObjects(ObjectFilter filter)
   {
      List result = new ArrayList();
      synchronized(objectList)
      {
         for(AbstractObject object : objectList.values())
         {
            if (filter.filter(object))
            {
               result.add(object);
            }
         }
      }
      return result;
   }

   /**
    * Get list of top-level objects matching given class filter. Class filter
    * may be null to ignore object class.
    *
    * @param classFilter To filter the classes
    * @return List of all top matching level objects (either without parents or with
    *         inaccessible parents)
    */
   public AbstractObject[] getTopLevelObjects(Set classFilter)
   {
      HashSet list = new HashSet();
      synchronized(objectList)
      {
         for(AbstractObject object : objectList.values())
         {
            if ((classFilter != null) && !classFilter.contains(object.getObjectClass())) continue;

            if (!object.hasParents())
            {
               list.add(object);
            }
            else
            {
               boolean hasParents = false;
               Iterator it = object.getParents();
               while(it.hasNext())
               {
                  Long parent = it.next();
                  if (classFilter != null)
                  {
                     AbstractObject p = objectList.get(parent);
                     if ((p != null) && classFilter.contains(p.getObjectClass()))
                     {
                        hasParents = true;
                        break;
                     }
                  }
                  else
                  {
                     if (objectList.containsKey(parent))
                     {
                        hasParents = true;
                        break;
                     }
                  }
               }
               if (!hasParents) list.add(object);
            }
         }
      }
      return list.toArray(new AbstractObject[list.size()]);
   }

   /**
    * Get list of top-level objects.
    *
    * @return List of all top level objects (either without parents or with
    *         inaccessible parents)
    */
   public AbstractObject[] getTopLevelObjects()
   {
      return getTopLevelObjects(null);
   }

   /**
    * Get list of all objects
    *
    * @return List of all objects
    */
   public List getAllObjects()
   {
      synchronized(objectList)
      {
         return new ArrayList(objectList.values());
      }
   }

   /**
    * Get object name by ID.
    *
    * @param objectId object ID
    * @return object name if object is known, or string in form [<object_id>] for unknown objects
    */
   public String getObjectName(long objectId)
   {
      AbstractObject object = findObjectById(objectId);
      return (object != null) ? object.getObjectName() : ("[" + Long.toString(objectId) + "]");
   }
   
   /**
    * Query objects on server side
    * 
    * @param query query to execute
    * @return list of matching objects
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public List queryObjects(String query) throws IOException, NXCException
   {
      NXCPMessage msg = newMessage(NXCPCodes.CMD_QUERY_OBJECTS);
      msg.setField(NXCPCodes.VID_QUERY, query);
      sendMessage(msg);
      NXCPMessage response = waitForRCC(msg.getMessageId());
      return findMultipleObjects(response.getFieldAsUInt32Array(NXCPCodes.VID_OBJECT_LIST), false);
   }

   /**
    * Get list of active alarms. For accessing terminated alarms log view API should be used.
    *
    * @return Hash map containing alarms
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public HashMap getAlarms() throws IOException, NXCException
   {
      NXCPMessage msg = newMessage(NXCPCodes.CMD_GET_ALL_ALARMS);
      final long rqId = msg.getMessageId();
      sendMessage(msg);

      final HashMap alarmList = new HashMap(0);
      while(true)
      {
         msg = waitForMessage(NXCPCodes.CMD_ALARM_DATA, rqId);
         long alarmId = msg.getFieldAsInt32(NXCPCodes.VID_ALARM_ID);
         if (alarmId == 0) 
            break; // ALARM_ID == 0 indicates end of list
         alarmList.put(alarmId, new Alarm(msg));
      }

      return alarmList;
   }

   /**
    * Get information about single active alarm. Terminated alarms cannot be accessed with this call.
    *
    * @param alarmId alarm ID
    * @return alarm object
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public Alarm getAlarm(long alarmId) throws IOException, NXCException
   {
      NXCPMessage msg = newMessage(NXCPCodes.CMD_GET_ALARM);
      msg.setFieldInt32(NXCPCodes.VID_ALARM_ID, (int) alarmId);
      sendMessage(msg);
      final NXCPMessage response = waitForRCC(msg.getMessageId());
      return new Alarm(response);
   }

   /**
    * Get information about events related to single active alarm. Information for terminated alarms cannot be accessed with this call.
    * User must have "view alarms" permission on alarm's source node and "view event log" system-wide access.
    *
    * @param alarmId alarm ID
    * @return list of related events
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public List getAlarmEvents(long alarmId) throws IOException, NXCException
   {
      NXCPMessage msg = newMessage(NXCPCodes.CMD_GET_ALARM_EVENTS);
      msg.setFieldInt32(NXCPCodes.VID_ALARM_ID, (int) alarmId);
      sendMessage(msg);
      final NXCPMessage response = waitForRCC(msg.getMessageId());

      int count = response.getFieldAsInt32(NXCPCodes.VID_NUM_ELEMENTS);
      List list = new ArrayList(count);
      long varId = NXCPCodes.VID_ELEMENT_LIST_BASE;
      for(int i = 0; i < count; i++)
      {
         EventInfo parent = null;
         long rootId = response.getFieldAsInt64(varId + 1);
         if (rootId != 0)
         {
            for(EventInfo e : list)
            {
               if (e.getId() == rootId)
               {
                  parent = e;
                  break;
               }
            }
         }
         list.add(new EventInfo(response, varId, parent));
         varId += 10;
      }
      return list;
   }

   /**
    * Acknowledge alarm.
    *
    * @param alarmId Identifier of alarm to be acknowledged.
    * @param sticky  if set to true, acknowledged state will be made "sticky" (duplicate alarms with same key will not revert it back to outstanding)
    * @param time timeout for sticky acknowledge in seconds (0 for infinite)
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public void acknowledgeAlarm(final long alarmId, boolean sticky, int time) throws IOException, NXCException
   {
      NXCPMessage msg = newMessage(NXCPCodes.CMD_ACK_ALARM);
      msg.setFieldInt32(NXCPCodes.VID_ALARM_ID, (int) alarmId);
      msg.setFieldInt16(NXCPCodes.VID_STICKY_FLAG, sticky ? 1 : 0);
      msg.setFieldInt32(NXCPCodes.VID_TIMESTAMP, time);
      sendMessage(msg);
      waitForRCC(msg.getMessageId());
   }

   /**
    * Acknowledge alarm.
    *
    * @param alarmId Identifier of alarm to be acknowledged.
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public void acknowledgeAlarm(final long alarmId) throws IOException, NXCException
   {
      acknowledgeAlarm(alarmId, false, 0);
   }

   /**
    * Acknowledge alarm by helpdesk reference.
    *
    * @param helpdeskReference Helpdesk issue reference (e.g. JIRA issue key)
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public void acknowledgeAlarm(String helpdeskReference) throws IOException, NXCException
   {
      NXCPMessage msg = newMessage(NXCPCodes.CMD_ACK_ALARM);
      msg.setField(NXCPCodes.VID_HELPDESK_REF, helpdeskReference);
      sendMessage(msg);
      waitForRCC(msg.getMessageId());
   }

   /**
    * Resolve alarm.
    *
    * @param alarmId Identifier of alarm to be resolved.
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public void resolveAlarm(final long alarmId) throws IOException, NXCException
   {
      NXCPMessage msg = newMessage(NXCPCodes.CMD_RESOLVE_ALARM);
      msg.setFieldInt32(NXCPCodes.VID_ALARM_ID, (int)alarmId);
      sendMessage(msg);
      waitForRCC(msg.getMessageId());
   }

   /**
    * Resolve alarm by helpdesk reference.
    *
    * @param helpdeskReference  Identifier of alarm to be resolved.
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public void resolveAlarm(final String helpdeskReference) throws IOException, NXCException
   {
      NXCPMessage msg = newMessage(NXCPCodes.CMD_RESOLVE_ALARM);
      msg.setField(NXCPCodes.VID_HELPDESK_REF, helpdeskReference);
      sendMessage(msg);
      waitForRCC(msg.getMessageId());
   }
   
   /**
    * Terminate alarm.
    *
    * @param alarmId Identifier of alarm to be terminated.
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public void terminateAlarm(final long alarmId) throws IOException, NXCException
   {
      NXCPMessage msg = newMessage(NXCPCodes.CMD_TERMINATE_ALARM);
      msg.setFieldInt32(NXCPCodes.VID_ALARM_ID, (int)alarmId);
      sendMessage(msg);
      waitForRCC(msg.getMessageId());
   }
   
   /**
    * Terminate alarm by helpdesk reference.
    *
    * @param helpdeskReference  Identifier of alarm to be resolved.
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public void terminateAlarm(final String helpdeskReference) throws IOException, NXCException
   {
      NXCPMessage msg = newMessage(NXCPCodes.CMD_TERMINATE_ALARM);
      msg.setField(NXCPCodes.VID_HELPDESK_REF, helpdeskReference);
      sendMessage(msg);
      waitForRCC(msg.getMessageId());
   }
   
   private Map bulkAlarmOperation(int cmd, List alarmIds) throws IOException, NXCException
   {
      NXCPMessage msg = newMessage(cmd);
      msg.setField(NXCPCodes.VID_ALARM_ID_LIST, alarmIds.toArray(new Long[alarmIds.size()]));
      sendMessage(msg);
      
      final NXCPMessage response = waitForRCC(msg.getMessageId());
      
      Map operationFails = new HashMap();
      
      // Returned alarm ID`s if there were any failed operations
      if (response.findField(NXCPCodes.VID_ALARM_ID_LIST) != null)
      {
         for(int i = 0; i < response.getFieldAsUInt32ArrayEx(NXCPCodes.VID_ALARM_ID_LIST).length; i++)
         {
            operationFails.put(response.getFieldAsUInt32ArrayEx(NXCPCodes.VID_ALARM_ID_LIST)[i], 
                  (Integer)response.getFieldAsUInt32ArrayEx(NXCPCodes.VID_FAIL_CODE_LIST)[i].intValue());
         }
      }
      
      
      return operationFails;
   }

   /**
    * Bulk terminate alarms.
    *
    * @param alarmIds Identifiers of alarms to be terminated.
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    * @return true if all alarms were terminated, false if some, or all, were not terminated
    */
   public Map bulkResolveAlarms(List alarmIds) throws IOException, NXCException
   {
      return bulkAlarmOperation(NXCPCodes.CMD_BULK_RESOLVE_ALARMS, alarmIds);
   }

   /**
    * Bulk terminate alarms.
    *
    * @param alarmIds Identifiers of alarms to be terminated.
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    * @return true if all alarms were terminated, false if some, or all, were not terminated
    */
   public Map bulkTerminateAlarms(List alarmIds) throws IOException, NXCException
   {
      return bulkAlarmOperation(NXCPCodes.CMD_BULK_TERMINATE_ALARMS, alarmIds);
   }

   /**
    * Delete alarm.
    *
    * @param alarmId Identifier of alarm to be deleted.
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public void deleteAlarm(final long alarmId) throws IOException, NXCException
   {
      NXCPMessage msg = newMessage(NXCPCodes.CMD_DELETE_ALARM);
      msg.setFieldInt32(NXCPCodes.VID_ALARM_ID, (int) alarmId);
      sendMessage(msg);
      waitForRCC(msg.getMessageId());
   }

   /**
    * Open issue in helpdesk system from given alarm
    *
    * @param alarmId alarm identifier
    * @return helpdesk issue identifier
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public String openHelpdeskIssue(long alarmId) throws IOException, NXCException
   {
      NXCPMessage msg = newMessage(NXCPCodes.CMD_OPEN_HELPDESK_ISSUE);
      msg.setFieldInt32(NXCPCodes.VID_ALARM_ID, (int)alarmId);
      sendMessage(msg);
      return waitForRCC(msg.getMessageId()).getFieldAsString(NXCPCodes.VID_HELPDESK_REF);
   }
   
   /**
    * Get URL for helpdesk issue associated with given alarm
    * 
    * @param alarmId The ID of alarm
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    * @return URL of helpdesk issue
    */
   public String getHelpdeskIssueUrl(long alarmId) throws IOException, NXCException
   {
      NXCPMessage msg = newMessage(NXCPCodes.CMD_GET_HELPDESK_URL);
      msg.setFieldInt32(NXCPCodes.VID_ALARM_ID, (int)alarmId);
      sendMessage(msg);
      return waitForRCC(msg.getMessageId()).getFieldAsString(NXCPCodes.VID_URL);
   }
   
   /**
    * Unlink helpdesk issue from alarm. User must have OBJECT_ACCESS_UPDATE_ALARMS access right
    * on alarm's source object and SYSTEM_ACCESS_UNLINK_ISSUES system wide access right.
    * 
    * @param helpdeskReference The helpdesk reference
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public void unlinkHelpdeskIssue(String helpdeskReference) throws IOException, NXCException
   {
      NXCPMessage msg = newMessage(NXCPCodes.CMD_UNLINK_HELPDESK_ISSUE);
      msg.setField(NXCPCodes.VID_HELPDESK_REF, helpdeskReference);
      sendMessage(msg);
      waitForRCC(msg.getMessageId());
   }

   /**
    * Unlink helpdesk issue from alarm. User must have OBJECT_ACCESS_UPDATE_ALARMS access right
    * on alarm's source object and SYSTEM_ACCESS_UNLINK_ISSUES system wide access right.
    * 
    * @param alarmId alarm id
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public void unlinkHelpdeskIssue(long alarmId) throws IOException, NXCException
   {
      NXCPMessage msg = newMessage(NXCPCodes.CMD_UNLINK_HELPDESK_ISSUE);
      msg.setFieldInt32(NXCPCodes.VID_ALARM_ID, (int)alarmId);
      sendMessage(msg);
      waitForRCC(msg.getMessageId());
   }

   /**
    * Get list of comments for given alarm.
    *
    * @param alarmId alarm ID
    * @return list of alarm comments
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public List getAlarmComments(long alarmId) throws IOException, NXCException
   {
      NXCPMessage msg = newMessage(NXCPCodes.CMD_GET_ALARM_COMMENTS);
      msg.setFieldInt32(NXCPCodes.VID_ALARM_ID, (int) alarmId);
      sendMessage(msg);

      final NXCPMessage response = waitForRCC(msg.getMessageId());
      int count = response.getFieldAsInt32(NXCPCodes.VID_NUM_ELEMENTS);
      final List comments = new ArrayList(count);
      long varId = NXCPCodes.VID_ELEMENT_LIST_BASE;
      for(int i = 0; i < count; i++)
      {
         comments.add(new AlarmComment(response, varId));
         varId += 10;
      }
      return comments;
   }

   /**
    * Delete alarm comment. 
    *
    * @param alarmId alarm ID
    * @param commentId  comment ID
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public void deleteAlarmComment(long alarmId, long commentId) throws IOException, NXCException
   {
      NXCPMessage msg = newMessage(NXCPCodes.CMD_DELETE_ALARM_COMMENT);
      msg.setFieldInt32(NXCPCodes.VID_ALARM_ID, (int) alarmId);
      msg.setFieldInt32(NXCPCodes.VID_COMMENT_ID, (int) commentId);
      sendMessage(msg);
      waitForRCC(msg.getMessageId());
   }
   
   /**
    * Create alarm comment.
    * 
    * @param alarmId The alarm ID
    * @param text The comment text
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public void createAlarmComment(long alarmId, String text) throws IOException, NXCException 
   {
   	updateAlarmComment(alarmId, 0, text);
   }

   /**
    * Create alarm comment by helpdesk reference.
    * 
    * @param helpdeskReference The helpdesk reference
    * @param text The reference text
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public void createAlarmComment(final String helpdeskReference, String text) throws IOException, NXCException 
   {
      NXCPMessage msg = newMessage(NXCPCodes.CMD_UPDATE_ALARM_COMMENT);
      msg.setFieldInt32(NXCPCodes.VID_ALARM_ID, 0);
      msg.setField(NXCPCodes.VID_HELPDESK_REF, helpdeskReference);
      msg.setField(NXCPCodes.VID_COMMENTS, text);
      sendMessage(msg);
      waitForRCC(msg.getMessageId());
   }

   /**
    * Update alarm comment.
    * If alarmId == 0 — new comment will be created.
    *
    * @param alarmId alarm ID
    * @param commentId  comment ID or 0 for creating new comment
    * @param text    message text
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public void updateAlarmComment(long alarmId, long commentId, String text) throws IOException, NXCException
   {
      NXCPMessage msg = newMessage(NXCPCodes.CMD_UPDATE_ALARM_COMMENT);
      msg.setFieldInt32(NXCPCodes.VID_ALARM_ID, (int) alarmId);
      msg.setFieldInt32(NXCPCodes.VID_COMMENT_ID, (int) commentId);
      msg.setField(NXCPCodes.VID_COMMENTS, text);
      sendMessage(msg);
      waitForRCC(msg.getMessageId());
   }
   
   /**
    * Changes state of alarm status flow. Strict or not - terminate state can be set only after 
    * resolve state or after any state. 
    * 
    * @param state state of alarm status flow - strict or not (1 or 0)
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public void setAlarmFlowState(int state) throws IOException, NXCException
   {
      NXCPMessage msg = newMessage(NXCPCodes.CMD_SET_ALARM_STATUS_FLOW);
      msg.setFieldInt32(NXCPCodes.VID_ALARM_STATUS_FLOW_STATE, state);
      sendMessage(msg);
      waitForRCC(msg.getMessageId());
   }

   /**
    * Get server configuration variables
    * 
    * @return The server variables
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public Map getServerVariables() throws IOException, NXCException
   {
      NXCPMessage request = newMessage(NXCPCodes.CMD_GET_CONFIG_VARLIST);
      sendMessage(request);

      final NXCPMessage response = waitForRCC(request.getMessageId());

      int count = response.getFieldAsInt32(NXCPCodes.VID_NUM_VARIABLES);
      final HashMap varList = new HashMap(count);

      long id = NXCPCodes.VID_VARLIST_BASE;
      for(int i = 0; i < count; i++, id += 10)
      {
         ServerVariable v = new ServerVariable(response, id);
         varList.put(v.getName(), v);
      }
      
      count = response.getFieldAsInt32(NXCPCodes.VID_NUM_VALUES);
      for(int i = 0; i < count; i++)
      {
         ServerVariable var = varList.get(response.getFieldAsString(id++));
         if (var != null)
            var.addPossibleValue(response, id);
         id += 2;
      }

      return varList;
   }
   
   /**
    * Get server public configuration variable
    * 
    * @param name configuration variable name
    * @return value of requested configuration variable
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public String getPublicServerVariable(String name) throws IOException, NXCException
   {
      NXCPMessage msg = newMessage(NXCPCodes.CMD_GET_PUBLIC_CONFIG_VAR);
      msg.setField(NXCPCodes.VID_NAME, name);
      sendMessage(msg);
      NXCPMessage response = waitForRCC(msg.getMessageId());
      return response.getFieldAsString(NXCPCodes.VID_VALUE);
   }
   
   /**
    * Get server public configuration variable as a int
    * 
    * @param name configuration variable name
    * @return value of requested configuration variable
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public int getPublicServerVariableAsInt(String name) throws IOException, NXCException
   {
      return Integer.parseInt(getPublicServerVariable(name));
   }

   /**
    * Get server public configuration variable as boolen value
    * 
    * @param name configuration variable name
    * @return value of requested configuration variable
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public boolean getPublicServerVariableAsBoolean(String name) throws IOException, NXCException
   {
      String value = getPublicServerVariable(name);
      if ((value.equalsIgnoreCase("true")) || (value.equalsIgnoreCase("yes")))
         return true;
      if ((value.equalsIgnoreCase("false")) || (value.equalsIgnoreCase("no")))
         return false;
      try
      {
         int n = Integer.parseInt(value);
         return n != 0;
      }
      catch(NumberFormatException e)
      {
         return false;
      }
   }
   
   /**
    * Set server configuration variable
    * 
    * @param name The name of the variable
    * @param value The value of the variable
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public void setServerVariable(final String name, final String value) throws IOException, NXCException
   {
      NXCPMessage msg = newMessage(NXCPCodes.CMD_SET_CONFIG_VARIABLE);
      msg.setField(NXCPCodes.VID_NAME, name);
      msg.setField(NXCPCodes.VID_VALUE, value);
      sendMessage(msg);
      waitForRCC(msg.getMessageId());
   }

   /**
    * Delete server configuration variable
    * 
    * @param name The name of the variable
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public void deleteServerVariable(final String name) throws IOException, NXCException
   {
      NXCPMessage msg = newMessage(NXCPCodes.CMD_DELETE_CONFIG_VARIABLE);
      msg.setField(NXCPCodes.VID_NAME, name);
      sendMessage(msg);
      waitForRCC(msg.getMessageId());
   }

   /**
    * Set server configuration variables to default
    * 
    * @param varList The list of variables
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public void setDefaultServerValues(List varList) throws IOException, NXCException
   {
      NXCPMessage msg = newMessage(NXCPCodes.CMD_SET_CONFIG_TO_DEFAULT);
      long base = NXCPCodes.VID_VARLIST_BASE;
      for(ServerVariable v : varList)
         msg.setField(base++, v.getName());
      msg.setFieldInt32(NXCPCodes.VID_NUM_VARIABLES, varList.size());
      sendMessage(msg);
      waitForRCC(msg.getMessageId());
   }

   /**
    * Get server config CLOB
    *
    * @param name The name of the config
    * @return The config CLOB
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public String getServerConfigClob(final String name) throws IOException, NXCException
   {
      final NXCPMessage msg = newMessage(NXCPCodes.CMD_CONFIG_GET_CLOB);
      msg.setField(NXCPCodes.VID_NAME, name);
      sendMessage(msg);
      final NXCPMessage response = waitForRCC(msg.getMessageId());
      return response.getFieldAsString(NXCPCodes.VID_VALUE);
   }

   /**
    * Set server config CLOB
    *
    * @param name The name to set
    * @param value The value to set
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public void setServerConfigClob(final String name, final String value) throws IOException, NXCException
   {
      NXCPMessage msg = newMessage(NXCPCodes.CMD_CONFIG_SET_CLOB);
      msg.setField(NXCPCodes.VID_NAME, name);
      msg.setField(NXCPCodes.VID_VALUE, value);
      sendMessage(msg);
      waitForRCC(msg.getMessageId());
   }
   
   /**
    * Subscribe to notification channel. Each subscribe call should be matched by unsubscribe call.
    * Calling subscribe on already subscribed channel will increase internal counter, and subscription
    * will be cancelled when this counter returns back to 0.
    *
    * @param channel Notification channel to subscribe to.
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public void subscribe(String channel) throws IOException, NXCException
   {
      NXCPMessage msg = newMessage(NXCPCodes.CMD_CHANGE_SUBSCRIPTION);
      msg.setField(NXCPCodes.VID_NAME, channel);
      msg.setFieldInt16(NXCPCodes.VID_OPERATION, 1);
      sendMessage(msg);
      waitForRCC(msg.getMessageId());
   }

   /**
    * Unsubscribe from notification channel.
    *
    * @param channel Notification channel to unsubscribe from.
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public void unsubscribe(String channel) throws IOException, NXCException
   {
      NXCPMessage msg = newMessage(NXCPCodes.CMD_CHANGE_SUBSCRIPTION);
      msg.setField(NXCPCodes.VID_NAME, channel);
      msg.setFieldInt16(NXCPCodes.VID_OPERATION, 0);
      sendMessage(msg);
      waitForRCC(msg.getMessageId());
   }

   /**
    * Synchronize user database and subscribe to user change notifications
    * 
    * @throws IOException
    *            if socket I/O error occurs
    * @throws NXCException
    *            if NetXMS server returns an error or operation was timed out
    */
   public void syncUserDatabase() throws IOException, NXCException
   {
      syncUserDB.acquireUninterruptibly();
      NXCPMessage msg = newMessage(NXCPCodes.CMD_LOAD_USER_DB);
      sendMessage(msg);
      waitForRCC(msg.getMessageId());
      waitForSync(syncUserDB, commandTimeout * 10);
      subscribe(CHANNEL_USERDB);
   }

   /**
    * Find user by ID
    * 
    * @param id The user DBObject Id
    * @return User object with given ID or null if such user does not exist
    */
   public AbstractUserObject findUserDBObjectById(final long id)
   {
      AbstractUserObject object;

      synchronized(userDB)
      {
         object = userDB.get(id);
      }
      return object;
   }

   /**
    * Get list of all user database objects
    * 
    * @return List of all user database objects
    */
   public AbstractUserObject[] getUserDatabaseObjects()
   {
      AbstractUserObject[] list;

      synchronized(userDB)
      {
         Collection values = userDB.values();
         list = values.toArray(new AbstractUserObject[values.size()]);
      }
      return list;
   }

   /**
    * Create user or group on server
    *
    * @param name Login name for new user
    * @return ID assigned to newly created user
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   private long createUserDBObject(final String name, final boolean isGroup) throws IOException, NXCException
   {
      final NXCPMessage msg = newMessage(NXCPCodes.CMD_CREATE_USER);
      msg.setField(NXCPCodes.VID_USER_NAME, name);
      msg.setField(NXCPCodes.VID_IS_GROUP, isGroup);
      sendMessage(msg);

      final NXCPMessage response = waitForRCC(msg.getMessageId());
      return response.getFieldAsInt64(NXCPCodes.VID_USER_ID);
   }

   /**
    * Create user on server
    * 
    * @param name
    *           Login name for new user
    * @return ID assigned to newly created user
    * @throws IOException
    *            if socket I/O error occurs
    * @throws NXCException
    *            if NetXMS server returns an error or operation was timed out
    */
   public long createUser(final String name) throws IOException, NXCException
   {
      return createUserDBObject(name, false);
   }

   /**
    * Create user group on server
    * 
    * @param name
    *           Name for new user group
    * @return ID assigned to newly created user group
    * @throws IOException
    *            if socket I/O error occurs
    * @throws NXCException
    *            if NetXMS server returns an error or operation was timed out
    */
   public long createUserGroup(final String name) throws IOException, NXCException
   {
      return createUserDBObject(name, true);
   }

   /**
    * Delete user or group on server
    * 
    * @param id
    *           User or group ID
    * @throws IOException
    *            if socket I/O error occurs
    * @throws NXCException
    *            if NetXMS server returns an error or operation was timed out
    */
   public void deleteUserDBObject(final long id) throws IOException, NXCException
   {
      NXCPMessage msg = newMessage(NXCPCodes.CMD_DELETE_USER);
      msg.setFieldInt32(NXCPCodes.VID_USER_ID, (int) id);
      sendMessage(msg);
      waitForRCC(msg.getMessageId());
   }

   /**
    * Set password for user
    * 
    * @param id User ID
    * @param newPassword New password
    * @param oldPassword Old password
    * @throws IOException if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public void setUserPassword(final long id, final String newPassword, final String oldPassword) throws IOException, NXCException
   {
      NXCPMessage msg = newMessage(NXCPCodes.CMD_SET_PASSWORD);
      msg.setFieldInt32(NXCPCodes.VID_USER_ID, (int) id);
      msg.setField(NXCPCodes.VID_PASSWORD, newPassword);
      if (oldPassword != null) 
         msg.setField(NXCPCodes.VID_OLD_PASSWORD, oldPassword);
      sendMessage(msg);
      waitForRCC(msg.getMessageId());
   }
   
   /**
    * Validate password for currently logged in user
    * 
    * @param password password to validate
    * @return true if password is valid
    * @throws IOException if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public boolean validateUserPassword(String password) throws IOException, NXCException
   {
      NXCPMessage msg = newMessage(NXCPCodes.CMD_VALIDATE_PASSWORD);
      msg.setField(NXCPCodes.VID_PASSWORD, password);
      sendMessage(msg);
      NXCPMessage response = waitForRCC(msg.getMessageId());
      return response.getFieldAsBoolean(NXCPCodes.VID_PASSWORD_IS_VALID);
   }

   /**
    * Modify user database object
    * 
    * @param object User data
    * @param fields bit mask indicating fields to modify
    * @throws IOException
    *            if socket I/O error occurs
    * @throws NXCException
    *            if NetXMS server returns an error or operation was timed out
    */
   public void modifyUserDBObject(final AbstractUserObject object, final int fields) throws IOException, NXCException
   {
      NXCPMessage msg = newMessage(NXCPCodes.CMD_UPDATE_USER);
      msg.setFieldInt32(NXCPCodes.VID_FIELDS, fields);
      object.fillMessage(msg);
      sendMessage(msg);
      waitForRCC(msg.getMessageId());
   }

   /**
    * Modify user database object
    * 
    * @param object User data
    * @throws IOException
    *            if socket I/O error occurs
    * @throws NXCException
    *            if NetXMS server returns an error or operation was timed out
    */
   public void detachUserFromLdap(final AbstractUserObject object) throws IOException, NXCException
   {
      NXCPMessage msg = newMessage(NXCPCodes.CMD_DETACH_LDAP_USER);
      msg.setFieldInt32(NXCPCodes.VID_USER_ID, (int)object.getId());
      sendMessage(msg);
      waitForRCC(msg.getMessageId());
   }
   
   /**
    * Modify user database object
    * 
    * @param object User data
    * @throws IOException
    *            if socket I/O error occurs
    * @throws NXCException
    *            if NetXMS server returns an error or operation was timed out
    */
   public void modifyUserDBObject(final AbstractUserObject object) throws IOException, NXCException
   {
      modifyUserDBObject(object, 0x7FFFFFFF);
   }

   /**
    * Lock user database
    * 
    * @throws IOException
    *            if socket I/O error occurs
    * @throws NXCException
    *            if NetXMS server returns an error or operation was timed out
    */
   public void lockUserDatabase() throws IOException, NXCException
   {
      NXCPMessage msg = newMessage(NXCPCodes.CMD_LOCK_USER_DB);
      sendMessage(msg);
      waitForRCC(msg.getMessageId());
   }

   /**
    * Unlock user database
    * 
    * @throws IOException if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public void unlockUserDatabase() throws IOException, NXCException
   {
      NXCPMessage msg = newMessage(NXCPCodes.CMD_UNLOCK_USER_DB);
      sendMessage(msg);
      waitForRCC(msg.getMessageId());
   }

   /**
    * Set custom attribute for currently logged in user. Server will allow to change
    * only attributes whose name starts with dot.
    * 
    * @param name Attribute's name
    * @param value New attribute's value
    * @throws IOException if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public void setAttributeForCurrentUser(final String name, final String value) throws IOException, NXCException
   {
      NXCPMessage msg = newMessage(NXCPCodes.CMD_SET_CURRENT_USER_ATTR);
      msg.setField(NXCPCodes.VID_NAME, name);
      msg.setField(NXCPCodes.VID_VALUE, value);
      sendMessage(msg);
      waitForRCC(msg.getMessageId());
   }

   /**
    * Get custom attribute for currently logged in user. If attribute is not set, empty string will
    * be returned.
    * 
    * @param name Attribute's name
    * @return Attribute's value
    * @throws IOException if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public String getAttributeForCurrentUser(final String name) throws IOException, NXCException
   {
      NXCPMessage msg = newMessage(NXCPCodes.CMD_GET_CURRENT_USER_ATTR);
      msg.setField(NXCPCodes.VID_NAME, name);
      sendMessage(msg);
      final NXCPMessage response = waitForRCC(msg.getMessageId());
      return response.getFieldAsString(NXCPCodes.VID_VALUE);
   }

   /**
    * Get last DCI values for given node
    *
    * @param nodeId                ID of the node to get DCI values for
    * @param objectTooltipOnly     if set to true, only DCIs with DCF_SHOW_ON_OBJECT_TOOLTIP flag set are returned
    * @param overviewOnly          if set to true, only DCIs with DCF_SHOW_IN_OBJECT_OVERVIEW flag set are returned
    * @param includeNoValueObjects if set to true, objects with no value (like instance discovery DCIs) will be returned as well
    * @return List of DCI values
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public DciValue[] getLastValues(final long nodeId, boolean objectTooltipOnly, boolean overviewOnly, boolean includeNoValueObjects) throws IOException, NXCException
   {
      final NXCPMessage msg = newMessage(NXCPCodes.CMD_GET_LAST_VALUES);
      msg.setFieldInt32(NXCPCodes.VID_OBJECT_ID, (int) nodeId);
      msg.setField(NXCPCodes.VID_OBJECT_TOOLTIP_ONLY, objectTooltipOnly);
      msg.setField(NXCPCodes.VID_OVERVIEW_ONLY, overviewOnly);
      msg.setField(NXCPCodes.VID_INCLUDE_NOVALUE_OBJECTS, includeNoValueObjects);
      sendMessage(msg);
      
      final NXCPMessage response = waitForRCC(msg.getMessageId());

      int count = response.getFieldAsInt32(NXCPCodes.VID_NUM_ITEMS);
      DciValue[] list = new DciValue[count];
      long base = NXCPCodes.VID_DCI_VALUES_BASE;
      for(int i = 0; i < count; i++, base += 50)
      {
         list[i] = DciValue.createFromMessage(nodeId, response, base);
      }

      return list;
   }

   /**
    * Get last DCI values for given node
    *
    * @param nodeId ID of the node to get DCI values for
    * @return List of DCI values
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public DciValue[] getLastValues(final long nodeId) throws IOException, NXCException
   {
      return getLastValues(nodeId, false, false, false);
   }
   
   /**
    * Get last DCI values for given Map DCI Instance list
    *
    * @param mapDcis List with Map DCI Instances
    * @return List of DCI values
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public DciValue[] getLastValues(Set mapDcis) throws IOException, NXCException
   {
      final NXCPMessage msg = newMessage(NXCPCodes.CMD_GET_DCI_VALUES);
      long base = NXCPCodes.VID_DCI_VALUES_BASE;
      msg.setFieldInt32(NXCPCodes.VID_NUM_ITEMS, mapDcis.size());
      for(MapDCIInstance item : mapDcis)
      {
         item.fillMessage(msg, base);
         base += 10;
      }

      return sendLastValuesMsg(msg);
   }
   
   /**
    * Get last DCI values for given Single Dci Config list
    *
    * @param dciConfig List with Single Dci Configs
    * @return List of DCI values
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public DciValue[] getLastValues(List dciConfig) throws IOException, NXCException
   {
      final NXCPMessage msg = newMessage(NXCPCodes.CMD_GET_DCI_VALUES);
      long base = NXCPCodes.VID_DCI_VALUES_BASE;
      msg.setFieldInt32(NXCPCodes.VID_NUM_ITEMS, dciConfig.size());
      for(SingleDciConfig c : dciConfig)
      {
         c.fillMessage(msg, base);
         base += 10;
      }

      return sendLastValuesMsg(msg);
   }
   
   /**
    * Send msg containing list of dci configurations
    * 
    * @param msg The NXCPMessage to send
    * @return The DCI values
    * @throws IOException if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public DciValue[] sendLastValuesMsg(NXCPMessage msg) throws IOException, NXCException
   {
      sendMessage(msg);
      final NXCPMessage response = waitForRCC(msg.getMessageId());

      int count = response.getFieldAsInt32(NXCPCodes.VID_NUM_ITEMS);
      DciValue[] list = new DciValue[count];
      long base = NXCPCodes.VID_DCI_VALUES_BASE;
      for(int i = 0; i < count; i++, base += 10)
      {
         list[i] = (DciValue)new SimpleDciValue(response, base);
      }

      return list;
   }
   
   /**
    * Get active thresholds
    * 
    * @param dciConfig Dci config
    * @return list of active thresholds
    * @throws IOException
    * @throws NXCException
    */
   public List getActiveThresholds(List dciConfig) throws IOException, NXCException
   {
      final NXCPMessage msg = newMessage(NXCPCodes.CMD_GET_ACTIVE_THRESHOLDS);
      long base = NXCPCodes.VID_DCI_VALUES_BASE;
      msg.setFieldInt32(NXCPCodes.VID_NUM_ITEMS, dciConfig.size());
      for(SingleDciConfig c : dciConfig)
      {
         c.fillMessage(msg, base);
         base += 10;
      }

      sendMessage(msg);
      final NXCPMessage response = waitForRCC(msg.getMessageId());

      int count = response.getFieldAsInt32(NXCPCodes.VID_NUM_THRESHOLDS);
      List list = new ArrayList(count);
      base = NXCPCodes.VID_DCI_THRESHOLD_BASE;
      for(int i = 0; i < count; i++, base += 20)
      {
         list.add(new Threshold(response, base));
      }

      return list;
   }

   /**
    * Get last values for given table DCI on given node
    *
    * @param nodeId ID of the node to get DCI values for
    * @param dciId  DCI ID
    * @return Table object with last values for table DCI
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public Table getTableLastValues(final long nodeId, final long dciId) throws IOException, NXCException
   {
      final NXCPMessage msg = newMessage(NXCPCodes.CMD_GET_TABLE_LAST_VALUES);
      msg.setFieldInt32(NXCPCodes.VID_OBJECT_ID, (int) nodeId);
      msg.setFieldInt32(NXCPCodes.VID_DCI_ID, (int) dciId);
      sendMessage(msg);

      final NXCPMessage response = waitForRCC(msg.getMessageId());
      return new Table(response);
   }

   /**
    * Get list of DCIs configured to be shown on performance tab in console for
    * given node.
    *
    * @param nodeId Node object ID
    * @return List of performance tab DCIs
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public PerfTabDci[] getPerfTabItems(final long nodeId) throws IOException, NXCException
   {
      final NXCPMessage msg = newMessage(NXCPCodes.CMD_GET_PERFTAB_DCI_LIST);
      msg.setFieldInt32(NXCPCodes.VID_OBJECT_ID, (int) nodeId);
      sendMessage(msg);

      final NXCPMessage response = waitForRCC(msg.getMessageId());

      int count = response.getFieldAsInt32(NXCPCodes.VID_NUM_ITEMS);
      PerfTabDci[] list = new PerfTabDci[count];
      long base = NXCPCodes.VID_SYSDCI_LIST_BASE;
      for(int i = 0; i < count; i++, base += 10)
      {
         list[i] = new PerfTabDci(response, base);
      }

      return list;
   }

   /**
    * Get threshold violation summary for all nodes under given parent object. Parent object could
    * be container, subnet, zone, entire network, or infrastructure service root.
    *
    * @param objectId parent object ID
    * @return list of threshold violation summary objects for all nodes below given root
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public List getThresholdSummary(final long objectId) throws IOException, NXCException
   {
      final NXCPMessage msg = newMessage(NXCPCodes.CMD_GET_THRESHOLD_SUMMARY);
      msg.setFieldInt32(NXCPCodes.VID_OBJECT_ID, (int) objectId);
      sendMessage(msg);

      final NXCPMessage response = waitForRCC(msg.getMessageId());

      List list = new ArrayList();
      long varId = NXCPCodes.VID_THRESHOLD_BASE;
      while(response.getFieldAsInt64(varId) != 0)
      {
         final ThresholdViolationSummary t = new ThresholdViolationSummary(response, varId);
         list.add(t);
         varId += 50 * t.getDciList().size() + 2;
      }

      return list;
   }

   /**
    * Parse data from raw message CMD_DCI_DATA.
    * This method is intended for calling by client internal methods only. It made public to
    * allow access from client extensions.
    *
    * @param input Raw data
    * @param data  Data object to add rows to
    * @return number of received data rows
    */
   public int parseDataRows(final byte[] input, DciData data)
   {
      final NXCPDataInputStream inputStream = new NXCPDataInputStream(input);
      int rows = 0;

      try
      {
         inputStream.skipBytes(4); // DCI ID
         rows = inputStream.readInt();
         final int dataType = inputStream.readInt();
         data.setDataType(dataType);
         inputStream.skipBytes(4); // padding

         for(int i = 0; i < rows; i++)
         {
            long timestamp = inputStream.readUnsignedInt() * 1000; // convert to
            // milliseconds

            switch(dataType)
            {
               case DataCollectionItem.DT_INT:
                  data.addDataRow(new DciDataRow(new Date(timestamp), new Long(inputStream.readInt())));
                  break;
               case DataCollectionItem.DT_UINT:
                  data.addDataRow(new DciDataRow(new Date(timestamp), new Long(inputStream.readUnsignedInt())));
                  break;
               case DataCollectionItem.DT_INT64:
               case DataCollectionItem.DT_UINT64:
                  inputStream.skipBytes(4); // padding
                  data.addDataRow(new DciDataRow(new Date(timestamp), new Long(inputStream.readLong())));
                  break;
               case DataCollectionItem.DT_FLOAT:
                  inputStream.skipBytes(4); // padding
                  data.addDataRow(new DciDataRow(new Date(timestamp), new Double(inputStream.readDouble())));
                  break;
               case DataCollectionItem.DT_STRING:
                  StringBuilder sb = new StringBuilder(256);
                  int count;
                  for(count = MAX_DCI_STRING_VALUE_LENGTH; count > 0; count--)
                  {
                     char ch = inputStream.readChar();
                     if (ch == 0)
                     {
                        count--;
                        break;
                     }
                     sb.append(ch);
                  }
                  inputStream.skipBytes(count * 2);
                  data.addDataRow(new DciDataRow(new Date(timestamp), sb.toString()));
                  break;
            }
         }
      }
      catch(IOException e)
      {
      }

      inputStream.close();
      return rows;
   }

   /**
    * Get collected DCI data from server. Please note that you should specify
    * either row count limit or time from/to limit.
    *
    * @param nodeId     Node ID
    * @param dciId      DCI ID
    * @param instance   instance value (for table DCI only)
    * @param dataColumn name of column to retrieve data from (for table DCI only)
    * @param from       Start of time range or null for no limit
    * @param to         End of time range or null for no limit
    * @param maxRows    Maximum number of rows to retrieve or 0 for no limit
    * @return DCI data set
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   private DciData getCollectedDataInternal(long nodeId, long dciId, String instance, String dataColumn, Date from, Date to, int maxRows) throws IOException, NXCException
   {
      NXCPMessage msg;
      if (instance != null) // table DCI
      {
         msg = newMessage(NXCPCodes.CMD_GET_TABLE_DCI_DATA);
         msg.setField(NXCPCodes.VID_INSTANCE, instance);
         msg.setField(NXCPCodes.VID_DATA_COLUMN, dataColumn);
      }
      else
      {
         msg = newMessage(NXCPCodes.CMD_GET_DCI_DATA);
      }
      msg.setFieldInt32(NXCPCodes.VID_OBJECT_ID, (int) nodeId);
      msg.setFieldInt32(NXCPCodes.VID_DCI_ID, (int) dciId);

      DciData data = new DciData(nodeId, dciId);

      int rowsReceived, rowsRemaining = maxRows;
      int timeFrom = (from != null) ? (int) (from.getTime() / 1000) : 0;
      int timeTo = (to != null) ? (int) (to.getTime() / 1000) : 0;

      do
      {
         msg.setMessageId(requestId.getAndIncrement());
         msg.setFieldInt32(NXCPCodes.VID_MAX_ROWS, maxRows);
         msg.setFieldInt32(NXCPCodes.VID_TIME_FROM, timeFrom);
         msg.setFieldInt32(NXCPCodes.VID_TIME_TO, timeTo);
         sendMessage(msg);

         waitForRCC(msg.getMessageId());

         NXCPMessage response = waitForMessage(NXCPCodes.CMD_DCI_DATA, msg.getMessageId());
         if (!response.isBinaryMessage()) 
            throw new NXCException(RCC.INTERNAL_ERROR);

         rowsReceived = parseDataRows(response.getBinaryData(), data);
         if (((rowsRemaining == 0) || (rowsRemaining > MAX_DCI_DATA_ROWS)) && (rowsReceived == MAX_DCI_DATA_ROWS))
         {
            // adjust boundaries for next request
            if (rowsRemaining > 0) rowsRemaining -= rowsReceived;

            // Rows goes in newest to oldest order, so if we need to
            // retrieve additional data, we should update timeTo limit
            if (to != null)
            {
               DciDataRow row = data.getLastValue();
               if (row != null)
               {
                  // There should be only one value per second, so we set
                  // last row's timestamp - 1 second as new boundary
                  timeTo = (int) (row.getTimestamp().getTime() / 1000) - 1;
               }
            }
         }
      }
      while(rowsReceived == MAX_DCI_DATA_ROWS);

      return data;
   }

   /**
    * Get collected DCI data from server. Please note that you should specify
    * either row count limit or time from/to limit.
    *
    * @param nodeId  Node ID
    * @param dciId   DCI ID
    * @param from    Start of time range or null for no limit
    * @param to      End of time range or null for no limit
    * @param maxRows Maximum number of rows to retrieve or 0 for no limit
    * @return DCI data set
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public DciData getCollectedData(long nodeId, long dciId, Date from, Date to, int maxRows) throws IOException, NXCException
   {
      return getCollectedDataInternal(nodeId, dciId, null, null, from, to, maxRows);
   }

   /**
    * Get collected table DCI data from server. Please note that you should specify
    * either row count limit or time from/to limit.
    *
    * @param nodeId     Node ID
    * @param dciId      DCI ID
    * @param instance   instance value
    * @param dataColumn name of column to retrieve data from
    * @param from       Start of time range or null for no limit
    * @param to         End of time range or null for no limit
    * @param maxRows    Maximum number of rows to retrieve or 0 for no limit
    * @return DCI data set
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public DciData getCollectedTableData(
      long nodeId, long dciId, String instance, String dataColumn, Date from, Date to, int maxRows)
      throws IOException, NXCException
   {
      if (instance == null || dataColumn == null) 
         throw new NXCException(RCC.INVALID_ARGUMENT);
      return getCollectedDataInternal(nodeId, dciId, instance, dataColumn, from, to, maxRows);
   }

   /**
    * Clear collected data for given DCI
    *
    * @param nodeId Node object ID
    * @param dciId  DCI ID
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public void clearCollectedData(long nodeId, long dciId) throws IOException, NXCException
   {
      final NXCPMessage msg = newMessage(NXCPCodes.CMD_CLEAR_DCI_DATA);
      msg.setFieldInt32(NXCPCodes.VID_OBJECT_ID, (int) nodeId);
      msg.setFieldInt32(NXCPCodes.VID_DCI_ID, (int) dciId);
      sendMessage(msg);
      waitForRCC(msg.getMessageId());
   }
   
   /**
    * Delete collected data entry for given DCI
    *
    * @param nodeId Node object ID
    * @param dciId  DCI ID
    * @param timestamp timestamp of entry
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public void deleteDciEntry(long nodeId, long dciId, long timestamp) throws IOException, NXCException
   {
      final NXCPMessage msg = newMessage(NXCPCodes.CMD_DELETE_DCI_ENTRY);
      msg.setFieldInt32(NXCPCodes.VID_OBJECT_ID, (int) nodeId);
      msg.setFieldInt32(NXCPCodes.VID_DCI_ID, (int) dciId);
      msg.setFieldInt32(NXCPCodes.VID_TIMESTAMP, (int)timestamp);
      sendMessage(msg);
      waitForRCC(msg.getMessageId());
   }

   /**
    * Force DCI poll for given DCI
    *
    * @param nodeId Node object ID
    * @param dciId  DCI ID
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public void forceDCIPoll(long nodeId, long dciId) throws IOException, NXCException
   {
      final NXCPMessage msg = newMessage(NXCPCodes.CMD_FORCE_DCI_POLL);
      msg.setFieldInt32(NXCPCodes.VID_OBJECT_ID, (int) nodeId);
      msg.setFieldInt32(NXCPCodes.VID_DCI_ID, (int) dciId);
      sendMessage(msg);
      waitForRCC(msg.getMessageId());
   }

   /**
    * Get list of thresholds configured for given DCI
    *
    * @param nodeId Node object ID
    * @param dciId  DCI ID
    * @return List of configured thresholds
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public Threshold[] getThresholds(final long nodeId, final long dciId) throws IOException, NXCException
   {
      final NXCPMessage msg = newMessage(NXCPCodes.CMD_GET_DCI_THRESHOLDS);
      msg.setFieldInt32(NXCPCodes.VID_OBJECT_ID, (int) nodeId);
      msg.setFieldInt32(NXCPCodes.VID_DCI_ID, (int) dciId);
      sendMessage(msg);

      final NXCPMessage response = waitForRCC(msg.getMessageId());
      int count = response.getFieldAsInt32(NXCPCodes.VID_NUM_THRESHOLDS);
      final Threshold[] list = new Threshold[count];

      long varId = NXCPCodes.VID_DCI_THRESHOLD_BASE;
      for(int i = 0; i < count; i++)
      {
         list[i] = new Threshold(response, varId);
         varId += 20;
      }

      return list;
   }

   /**
    * Get names for given DCI list
    *
    * @param nodeIds node identifiers
    * @param dciIds  DCI identifiers (length must match length of node identifiers list)
    * @return array of resolved DCI names
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public String[] dciIdsToNames(long[] nodeIds, long[] dciIds) throws IOException, NXCException
   {
      if (nodeIds.length == 0) return new String[0];

      final NXCPMessage msg = newMessage(NXCPCodes.CMD_RESOLVE_DCI_NAMES);
      msg.setFieldInt32(NXCPCodes.VID_NUM_ITEMS, nodeIds.length);
      msg.setField(NXCPCodes.VID_NODE_LIST, nodeIds);
      msg.setField(NXCPCodes.VID_DCI_LIST, dciIds);
      sendMessage(msg);

      final NXCPMessage response = waitForRCC(msg.getMessageId());
      String[] result = new String[nodeIds.length];
      long varId = NXCPCodes.VID_DCI_LIST_BASE;
      for(int i = 0; i < result.length; i++)
      {
         result[i] = response.getFieldAsString(varId++);
      }
      return result;
   }

   /**
    * Get names for given DCI list
    *
    * @param dciList DCI list
    * @return array of resolved DCI names
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public String[] dciIdsToNames(Collection dciList) throws IOException, NXCException
   {
      final long[] nodeIds = new long[dciList.size()];
      final long[] dciIds = new long[dciList.size()];
      int i = 0;
      for(ConditionDciInfo dci : dciList)
      {
         nodeIds[i] = dci.getNodeId();
         dciIds[i] = dci.getDciId();
         i++;
      }
      return dciIdsToNames(nodeIds, dciIds);
   }

   /**
    * Get DCI ID for given DCI name
    *
    * @param nodeId node object identifier
    * @param dciName  DCI name
    * @return id of DCI with given name
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public long dciNameToId(long nodeId, String dciName) throws IOException, NXCException
   {
      if (nodeId == 0 || dciName == null || dciName.isEmpty()) 
         return 0;
      
      DciValue[] list = getLastValues(nodeId);
      for(DciValue dciValue : list)
      {
         if(dciValue.getName().equals(dciName))
            return dciValue.getId();
      }
      return 0;
   }

   /**
    * Query parameter immediately. This call will cause server to do actual call
    * to managed node and will return current value for given parameter. Result
    * is not cached.
    *
    * @param nodeId node object ID
    * @param origin parameter's origin (NetXMS agent, SNMP, etc.)
    * @param name   parameter's name
    * @return current parameter's value
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public String queryParameter(long nodeId, int origin, String name) throws IOException, NXCException
   {
      final NXCPMessage msg = newMessage(NXCPCodes.CMD_QUERY_PARAMETER);
      msg.setFieldInt32(NXCPCodes.VID_OBJECT_ID, (int) nodeId);
      msg.setFieldInt16(NXCPCodes.VID_DCI_SOURCE_TYPE, origin);
      msg.setField(NXCPCodes.VID_NAME, name);
      sendMessage(msg);

      final NXCPMessage response = waitForRCC(msg.getMessageId());
      return response.getFieldAsString(NXCPCodes.VID_VALUE);
   }

   /**
    * Query agent's table immediately. This call will cause server to do actual
    * call to managed node and will return current value for given table. Result
    * is not cached.
    *
    * @param nodeId node object ID
    * @param name   table's name
    * @return current table's value
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public Table queryAgentTable(long nodeId, String name) throws IOException, NXCException
   {
      final NXCPMessage msg = newMessage(NXCPCodes.CMD_QUERY_TABLE);
      msg.setFieldInt32(NXCPCodes.VID_OBJECT_ID, (int) nodeId);
      msg.setField(NXCPCodes.VID_NAME, name);
      sendMessage(msg);

      final NXCPMessage response = waitForRCC(msg.getMessageId());
      return new Table(response);
   }

   /**
    * Hook method to allow adding of custom object creation data to NXCP message.
    * Default implementation does nothing.
    *
    * @param data     object creation data passed to createObject
    * @param userData user-defined data for object creation passed to createObject
    * @param msg      NXCP message that will be sent to server
    */
   protected void createCustomObject(NXCObjectCreationData data, Object userData, NXCPMessage msg)
   {
   }

   /**
    * Create new NetXMS object.
    *
    * @param data     Object creation data
    * @param userData User-defined data for custom object creation
    * @return ID of new object
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public long createObject(final NXCObjectCreationData data, final Object userData) throws IOException, NXCException
   {
      final NXCPMessage msg = newMessage(NXCPCodes.CMD_CREATE_OBJECT);

      // Common attributes
      msg.setFieldInt32(NXCPCodes.VID_PARENT_ID, (int) data.getParentId());
      msg.setFieldInt16(NXCPCodes.VID_OBJECT_CLASS, data.getObjectClass());
      msg.setField(NXCPCodes.VID_OBJECT_NAME, data.getName());
      msg.setFieldInt32(NXCPCodes.VID_ZONE_UIN, (int)data.getZoneUIN());
      if (data.getComments() != null) msg.setField(NXCPCodes.VID_COMMENTS, data.getComments());

      // Class-specific attributes
      switch(data.getObjectClass())
      {
         case AbstractObject.OBJECT_CHASSIS:
            msg.setFieldInt32(NXCPCodes.VID_CONTROLLER_ID, (int)data.getControllerId());
            break;
         case AbstractObject.OBJECT_INTERFACE:
            msg.setField(NXCPCodes.VID_MAC_ADDR, data.getMacAddress().getValue());
            msg.setField(NXCPCodes.VID_IP_ADDRESS, data.getIpAddress());
            msg.setFieldInt32(NXCPCodes.VID_IF_TYPE, data.getIfType());
            msg.setFieldInt32(NXCPCodes.VID_IF_INDEX, data.getIfIndex());
            msg.setFieldInt32(NXCPCodes.VID_IF_SLOT, data.getSlot());
            msg.setFieldInt32(NXCPCodes.VID_IF_PORT, data.getPort());
            msg.setFieldInt16(NXCPCodes.VID_IS_PHYS_PORT, data.isPhysicalPort() ? 1 : 0);
            break;
         case AbstractObject.OBJECT_MOBILEDEVICE:
            msg.setField(NXCPCodes.VID_DEVICE_ID, data.getDeviceId());
            break;
         case AbstractObject.OBJECT_NODE:
            if (data.getPrimaryName() != null) 
               msg.setField(NXCPCodes.VID_PRIMARY_NAME, data.getPrimaryName());
            msg.setField(NXCPCodes.VID_IP_ADDRESS, data.getIpAddress());
            msg.setFieldInt16(NXCPCodes.VID_AGENT_PORT, data.getAgentPort());
            msg.setFieldInt16(NXCPCodes.VID_SNMP_PORT, data.getSnmpPort());
            msg.setFieldInt32(NXCPCodes.VID_CREATION_FLAGS, data.getCreationFlags());
            msg.setFieldInt32(NXCPCodes.VID_AGENT_PROXY, (int)data.getAgentProxyId());
            msg.setFieldInt32(NXCPCodes.VID_SNMP_PROXY, (int)data.getSnmpProxyId());
            msg.setFieldInt32(NXCPCodes.VID_ICMP_PROXY, (int)data.getIcmpProxyId());
            msg.setFieldInt32(NXCPCodes.VID_SSH_PROXY, (int)data.getSshProxyId());
            msg.setFieldInt32(NXCPCodes.VID_CHASSIS_ID, (int)data.getChassisId());
            msg.setField(NXCPCodes.VID_SSH_LOGIN, data.getSshLogin());
            msg.setField(NXCPCodes.VID_SSH_PASSWORD, data.getSshPassword());
            break;
         case AbstractObject.OBJECT_NETWORKMAP:
            msg.setFieldInt16(NXCPCodes.VID_MAP_TYPE, data.getMapType());
            msg.setField(NXCPCodes.VID_SEED_OBJECTS, data.getSeedObjectIds());
            msg.setFieldInt32(NXCPCodes.VID_FLAGS, (int) data.getFlags());
            break;
         case AbstractObject.OBJECT_NETWORKSERVICE:
            msg.setFieldInt16(NXCPCodes.VID_SERVICE_TYPE, data.getServiceType());
            msg.setFieldInt16(NXCPCodes.VID_IP_PROTO, data.getIpProtocol());
            msg.setFieldInt16(NXCPCodes.VID_IP_PORT, data.getIpPort());
            msg.setField(NXCPCodes.VID_SERVICE_REQUEST, data.getRequest());
            msg.setField(NXCPCodes.VID_SERVICE_RESPONSE, data.getResponse());
            msg.setFieldInt16(NXCPCodes.VID_CREATE_STATUS_DCI, data.isCreateStatusDci() ? 1 : 0);
            break;
         case AbstractObject.OBJECT_NODELINK:
            msg.setFieldInt32(NXCPCodes.VID_NODE_ID, (int) data.getLinkedNodeId());
            break;
         case AbstractObject.OBJECT_RACK:
            msg.setFieldInt16(NXCPCodes.VID_HEIGHT, data.getHeight());
            break;
         case AbstractObject.OBJECT_SLMCHECK:
            msg.setFieldInt16(NXCPCodes.VID_IS_TEMPLATE, data.isTemplate() ? 1 : 0);
            break;
      }

      if (userData != null) createCustomObject(data, userData, msg);

      sendMessage(msg);
      final NXCPMessage response = waitForRCC(msg.getMessageId());
      return response.getFieldAsInt64(NXCPCodes.VID_OBJECT_ID);
   }

   /**
    * Create new NetXMS object. Equivalent of calling createObject(data, null).
    *
    * @param data Object creation data
    * @return ID of new object
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public long createObject(final NXCObjectCreationData data) throws IOException, NXCException
   {
      return createObject(data, null);
   }

   /**
    * Delete object
    *
    * @param objectId ID of an object which should be deleted
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public void deleteObject(final long objectId) throws IOException, NXCException
   {
      NXCPMessage msg = newMessage(NXCPCodes.CMD_DELETE_OBJECT);
      msg.setFieldInt32(NXCPCodes.VID_OBJECT_ID, (int) objectId);
      sendMessage(msg);
      waitForRCC(msg.getMessageId());

      // If server reports success, delete object from cache and generate
      // appropriate notification without waiting for actual server update
      synchronized(objectList)
      {
         AbstractObject object = objectList.get(objectId);
         if (object != null)
         {
            objectList.remove(objectId);
            objectListGUID.remove(object.getGuid());
            if (object instanceof Zone)
               zoneList.remove(((Zone)object).getUIN());
            removeOrphanedObjects(object);
         }
      }
      sendNotification(new SessionNotification(SessionNotification.OBJECT_DELETED, objectId));
   }
   
   /**
    * Remove orphaned objects (with last parent left)
    * 
    * @param parent
    */
   private void removeOrphanedObjects(AbstractObject parent)
   {
      Iterator it = parent.getChildren();
      while(it.hasNext())
      {
         AbstractObject object = objectList.get(it.next());
         if ((object != null) && (object.getParentCount() == 1))
         {
            objectList.remove(object.getObjectId());
            objectListGUID.remove(object.getGuid());
            if (object instanceof Zone)
               zoneList.remove(((Zone)object).getUIN());
            removeOrphanedObjects(object);
         }
      }
   }

   /**
    * Hook method to populate NXCP message with custom object's data on object modification.
    * Default implementation does nothing.
    *
    * @param data     object modification data passed to modifyObject
    * @param userData user-defined data passed to modifyObject
    * @param msg      NXCP message to be sent to server
    */
   protected void modifyCustomObject(NXCObjectModificationData data, Object userData, NXCPMessage msg)
   {
   }

   /**
    * Modify object (generic interface, in most cases wrapper functions should
    * be used instead).
    *
    * @param data     Object modification data
    * @param userData user-defined data for custom object modification
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public void modifyObject(final NXCObjectModificationData data, final Object userData) throws IOException, NXCException
   {
      NXCPMessage msg = newMessage(NXCPCodes.CMD_MODIFY_OBJECT);
      msg.setFieldInt32(NXCPCodes.VID_OBJECT_ID, (int) data.getObjectId());

      // Object name
      if (data.isFieldSet(NXCObjectModificationData.NAME))
      {
         msg.setField(NXCPCodes.VID_OBJECT_NAME, data.getName());
      }

      // Primary IP
      if (data.isFieldSet(NXCObjectModificationData.PRIMARY_IP))
      {
         msg.setField(NXCPCodes.VID_IP_ADDRESS, data.getPrimaryIpAddress());
      }

      // Access control list
      if (data.isFieldSet(NXCObjectModificationData.ACL))
      {
         final AccessListElement[] acl = data.getACL();
         msg.setFieldInt32(NXCPCodes.VID_ACL_SIZE, acl.length);
         msg.setFieldInt16(NXCPCodes.VID_INHERIT_RIGHTS, data.isInheritAccessRights() ? 1 : 0);

         long id1 = NXCPCodes.VID_ACL_USER_BASE;
         long id2 = NXCPCodes.VID_ACL_RIGHTS_BASE;
         for(int i = 0; i < acl.length; i++)
         {
            msg.setFieldInt32(id1++, (int) acl[i].getUserId());
            msg.setFieldInt32(id2++, acl[i].getAccessRights());
         }
      }

      if (data.isFieldSet(NXCObjectModificationData.CUSTOM_ATTRIBUTES))
      {
         Map attrList = data.getCustomAttributes();
         Iterator it = attrList.keySet().iterator();
         long id = NXCPCodes.VID_CUSTOM_ATTRIBUTES_BASE;
         int count = 0;
         while(it.hasNext())
         {
            String key = it.next();
            String value = attrList.get(key);
            msg.setField(id++, key);
            msg.setField(id++, value);
            count++;
         }
         msg.setFieldInt32(NXCPCodes.VID_NUM_CUSTOM_ATTRIBUTES, count);
      }

      if (data.isFieldSet(NXCObjectModificationData.AUTOBIND_FILTER))
      {
         msg.setField(NXCPCodes.VID_AUTOBIND_FILTER, data.getAutoBindFilter());
      }

      if (data.isFieldSet(NXCObjectModificationData.FILTER))
      {
         msg.setField(NXCPCodes.VID_FILTER, data.getFilter());
      }

      if (data.isFieldSet(NXCObjectModificationData.DESCRIPTION))
      {
         msg.setField(NXCPCodes.VID_DESCRIPTION, data.getDescription());
      }

      if (data.isFieldSet(NXCObjectModificationData.VERSION))
      {
         msg.setFieldInt32(NXCPCodes.VID_VERSION, data.getVersion());
      }

      // Configuration file
      if (data.isFieldSet(NXCObjectModificationData.POLICY_CONFIG))
      {
         msg.setField(NXCPCodes.VID_CONFIG_FILE_DATA, data.getConfigFileContent());
      }

      if (data.isFieldSet(NXCObjectModificationData.AGENT_PORT))
      {
         msg.setFieldInt16(NXCPCodes.VID_AGENT_PORT, data.getAgentPort());
      }

      if (data.isFieldSet(NXCObjectModificationData.AGENT_PROXY))
      {
         msg.setFieldInt32(NXCPCodes.VID_AGENT_PROXY, (int) data.getAgentProxy());
      }

      if (data.isFieldSet(NXCObjectModificationData.AGENT_AUTH))
      {
         msg.setFieldInt16(NXCPCodes.VID_AUTH_METHOD, data.getAgentAuthMethod());
         msg.setField(NXCPCodes.VID_SHARED_SECRET, data.getAgentSecret());
      }

      if (data.isFieldSet(NXCObjectModificationData.TRUSTED_NODES))
      {
         final long[] nodes = data.getTrustedNodes();
         msg.setFieldInt32(NXCPCodes.VID_NUM_TRUSTED_NODES, nodes.length);
         msg.setField(NXCPCodes.VID_TRUSTED_NODES, nodes);
      }

      if (data.isFieldSet(NXCObjectModificationData.SNMP_VERSION))
      {
         msg.setFieldInt16(NXCPCodes.VID_SNMP_VERSION, data.getSnmpVersion());
      }

      if (data.isFieldSet(NXCObjectModificationData.SNMP_AUTH))
      {
         msg.setField(NXCPCodes.VID_SNMP_AUTH_OBJECT, data.getSnmpAuthName());
         msg.setField(NXCPCodes.VID_SNMP_AUTH_PASSWORD, data.getSnmpAuthPassword());
         msg.setField(NXCPCodes.VID_SNMP_PRIV_PASSWORD, data.getSnmpPrivPassword());
         int methods = data.getSnmpAuthMethod() | (data.getSnmpPrivMethod() << 8);
         msg.setFieldInt16(NXCPCodes.VID_SNMP_USM_METHODS, methods);
      }

      if (data.isFieldSet(NXCObjectModificationData.SNMP_PROXY))
      {
         msg.setFieldInt32(NXCPCodes.VID_SNMP_PROXY, (int) data.getSnmpProxy());
      }

      if (data.isFieldSet(NXCObjectModificationData.SNMP_PORT))
      {
         msg.setFieldInt16(NXCPCodes.VID_SNMP_PORT, data.getSnmpPort());
      }

      if (data.isFieldSet(NXCObjectModificationData.ICMP_PROXY))
      {
         msg.setFieldInt32(NXCPCodes.VID_ICMP_PROXY, (int) data.getIcmpProxy());
      }

      if (data.isFieldSet(NXCObjectModificationData.GEOLOCATION))
      {
         final GeoLocation gl = data.getGeolocation();
         msg.setFieldInt16(NXCPCodes.VID_GEOLOCATION_TYPE, gl.getType());
         msg.setField(NXCPCodes.VID_LATITUDE, gl.getLatitude());
         msg.setField(NXCPCodes.VID_LONGITUDE, gl.getLongitude());
         msg.setFieldInt16(NXCPCodes.VID_ACCURACY, gl.getAccuracy());
         if (gl.getTimestamp() != null)
         {
            msg.setFieldInt64(NXCPCodes.VID_GEOLOCATION_TIMESTAMP, gl.getTimestamp().getTime() / 1000);
         }
      }

      if (data.isFieldSet(NXCObjectModificationData.MAP_LAYOUT))
      {
         msg.setFieldInt16(NXCPCodes.VID_LAYOUT, data.getMapLayout().getValue());
      }

      if (data.isFieldSet(NXCObjectModificationData.MAP_BACKGROUND))
      {
         msg.setField(NXCPCodes.VID_BACKGROUND, data.getMapBackground());
         msg.setField(NXCPCodes.VID_BACKGROUND_LATITUDE, data.getMapBackgroundLocation().getLatitude());
         msg.setField(NXCPCodes.VID_BACKGROUND_LONGITUDE, data.getMapBackgroundLocation().getLongitude());
         msg.setFieldInt16(NXCPCodes.VID_BACKGROUND_ZOOM, data.getMapBackgroundZoom());
         msg.setFieldInt32(NXCPCodes.VID_BACKGROUND_COLOR, data.getMapBackgroundColor());
      }

      if (data.isFieldSet(NXCObjectModificationData.IMAGE))
      {
         msg.setField(NXCPCodes.VID_IMAGE, data.getImage());
      }

      if (data.isFieldSet(NXCObjectModificationData.MAP_CONTENT))
      {
         msg.setFieldInt32(NXCPCodes.VID_NUM_ELEMENTS, data.getMapElements().size());
         long varId = NXCPCodes.VID_ELEMENT_LIST_BASE;
         for(NetworkMapElement e : data.getMapElements())
         {
            e.fillMessage(msg, varId);
            varId += 100;
         }

         msg.setFieldInt32(NXCPCodes.VID_NUM_LINKS, data.getMapLinks().size());
         varId = NXCPCodes.VID_LINK_LIST_BASE;
         for(NetworkMapLink l : data.getMapLinks())
         {
            l.fillMessage(msg, varId);
            varId += 20;
         }
      }

      if (data.isFieldSet(NXCObjectModificationData.COLUMN_COUNT))
      {
         msg.setFieldInt16(NXCPCodes.VID_NUM_COLUMNS, data.getColumnCount());
      }

      if (data.isFieldSet(NXCObjectModificationData.DASHBOARD_ELEMENTS))
      {
         msg.setFieldInt32(NXCPCodes.VID_NUM_ELEMENTS, data.getDashboardElements().size());
         long varId = NXCPCodes.VID_ELEMENT_LIST_BASE;
         for(DashboardElement e : data.getDashboardElements())
         {
            e.fillMessage(msg, varId);
            varId += 10;
         }
      }

      if (data.isFieldSet(NXCObjectModificationData.URL_LIST))
      {
         msg.setFieldInt32(NXCPCodes.VID_NUM_URLS, data.getUrls().size());
         long fieldId = NXCPCodes.VID_URL_LIST_BASE;
         for(ObjectUrl u : data.getUrls())
         {
            u.fillMessage(msg, fieldId);
            fieldId += 10;
         }
      }

      if (data.isFieldSet(NXCObjectModificationData.SCRIPT))
      {
         msg.setField(NXCPCodes.VID_SCRIPT, data.getScript());
      }

      if (data.isFieldSet(NXCObjectModificationData.ACTIVATION_EVENT))
      {
         msg.setFieldInt32(NXCPCodes.VID_ACTIVATION_EVENT, data.getActivationEvent());
      }

      if (data.isFieldSet(NXCObjectModificationData.DEACTIVATION_EVENT))
      {
         msg.setFieldInt32(NXCPCodes.VID_DEACTIVATION_EVENT, data.getDeactivationEvent());
      }

      if (data.isFieldSet(NXCObjectModificationData.SOURCE_OBJECT))
      {
         msg.setFieldInt32(NXCPCodes.VID_SOURCE_OBJECT, (int) data.getSourceObject());
      }

      if (data.isFieldSet(NXCObjectModificationData.ACTIVE_STATUS))
      {
         msg.setFieldInt16(NXCPCodes.VID_ACTIVE_STATUS, data.getActiveStatus());
      }

      if (data.isFieldSet(NXCObjectModificationData.INACTIVE_STATUS))
      {
         msg.setFieldInt16(NXCPCodes.VID_INACTIVE_STATUS, data.getInactiveStatus());
      }

      if (data.isFieldSet(NXCObjectModificationData.DCI_LIST))
      {
         List dciList = data.getDciList();
         msg.setFieldInt32(NXCPCodes.VID_NUM_ITEMS, dciList.size());
         long varId = NXCPCodes.VID_DCI_LIST_BASE;
         for(ConditionDciInfo dci : dciList)
         {
            msg.setFieldInt32(varId++, (int) dci.getDciId());
            msg.setFieldInt32(varId++, (int) dci.getNodeId());
            msg.setFieldInt16(varId++, dci.getFunction());
            msg.setFieldInt16(varId++, dci.getPolls());
            varId += 6;
         }
      }

      if (data.isFieldSet(NXCObjectModificationData.DRILL_DOWN_OBJECT_ID))
      {
         msg.setFieldInt32(NXCPCodes.VID_DRILL_DOWN_OBJECT_ID, (int) data.getDrillDownObjectId());
      }

      if (data.isFieldSet(NXCObjectModificationData.SERVICE_TYPE))
      {
         msg.setFieldInt16(NXCPCodes.VID_SERVICE_TYPE, data.getServiceType());
      }

      if (data.isFieldSet(NXCObjectModificationData.IP_ADDRESS))
      {
         msg.setField(NXCPCodes.VID_IP_ADDRESS, data.getIpAddress());
      }

      if (data.isFieldSet(NXCObjectModificationData.IP_PROTOCOL))
      {
         msg.setFieldInt16(NXCPCodes.VID_IP_PROTO, data.getIpProtocol());
      }

      if (data.isFieldSet(NXCObjectModificationData.IP_PORT))
      {
         msg.setFieldInt16(NXCPCodes.VID_IP_PORT, data.getIpPort());
      }

      if (data.isFieldSet(NXCObjectModificationData.POLLER_NODE))
      {
         msg.setFieldInt32(NXCPCodes.VID_POLLER_NODE_ID, (int) data.getPollerNode());
      }

      if (data.isFieldSet(NXCObjectModificationData.REQUIRED_POLLS))
      {
         msg.setFieldInt16(NXCPCodes.VID_REQUIRED_POLLS, data.getRequiredPolls());
      }

      if (data.isFieldSet(NXCObjectModificationData.REQUEST))
      {
         msg.setField(NXCPCodes.VID_SERVICE_REQUEST, data.getRequest());
      }

      if (data.isFieldSet(NXCObjectModificationData.RESPONSE))
      {
         msg.setField(NXCPCodes.VID_SERVICE_RESPONSE, data.getResponse());
      }

      if (data.isFieldSet(NXCObjectModificationData.OBJECT_FLAGS))
      {
         msg.setFieldInt32(NXCPCodes.VID_FLAGS, data.getObjectFlags());
         msg.setFieldInt32(NXCPCodes.VID_FLAGS_MASK, data.getObjectFlagsMask());
      }

      if (data.isFieldSet(NXCObjectModificationData.IFXTABLE_POLICY))
      {
         msg.setFieldInt16(NXCPCodes.VID_USE_IFXTABLE, data.getIfXTablePolicy());
      }

      if (data.isFieldSet(NXCObjectModificationData.REPORT_DEFINITION))
      {
         msg.setField(NXCPCodes.VID_REPORT_DEFINITION, data.getReportDefinition());
      }

      if (data.isFieldSet(NXCObjectModificationData.CLUSTER_RESOURCES))
      {
         msg.setFieldInt32(NXCPCodes.VID_NUM_RESOURCES, data.getResourceList().size());
         long varId = NXCPCodes.VID_RESOURCE_LIST_BASE;
         for(ClusterResource r : data.getResourceList())
         {
            msg.setFieldInt32(varId++, (int) r.getId());
            msg.setField(varId++, r.getName());
            msg.setField(varId++, r.getVirtualAddress());
            varId += 7;
         }
      }

      if (data.isFieldSet(NXCObjectModificationData.CLUSTER_NETWORKS))
      {
         int count = data.getNetworkList().size();
         msg.setFieldInt32(NXCPCodes.VID_NUM_SYNC_SUBNETS, count);
         long varId = NXCPCodes.VID_SYNC_SUBNETS_BASE;
         for(InetAddressEx n : data.getNetworkList())
         {
            msg.setField(varId++, n);
         }
      }

      if (data.isFieldSet(NXCObjectModificationData.PRIMARY_NAME))
      {
         msg.setField(NXCPCodes.VID_PRIMARY_NAME, data.getPrimaryName());
      }

      if (data.isFieldSet(NXCObjectModificationData.STATUS_CALCULATION))
      {
         msg.setFieldInt16(NXCPCodes.VID_STATUS_CALCULATION_ALG, data.getStatusCalculationMethod());
         msg.setFieldInt16(NXCPCodes.VID_STATUS_PROPAGATION_ALG, data.getStatusPropagationMethod());
         msg.setFieldInt16(NXCPCodes.VID_FIXED_STATUS, data.getFixedPropagatedStatus().getValue());
         msg.setFieldInt16(NXCPCodes.VID_STATUS_SHIFT, data.getStatusShift());
         ObjectStatus[] transformation = data.getStatusTransformation();
         msg.setFieldInt16(NXCPCodes.VID_STATUS_TRANSLATION_1, transformation[0].getValue());
         msg.setFieldInt16(NXCPCodes.VID_STATUS_TRANSLATION_2, transformation[1].getValue());
         msg.setFieldInt16(NXCPCodes.VID_STATUS_TRANSLATION_3, transformation[2].getValue());
         msg.setFieldInt16(NXCPCodes.VID_STATUS_TRANSLATION_4, transformation[3].getValue());
         msg.setFieldInt16(NXCPCodes.VID_STATUS_SINGLE_THRESHOLD, data.getStatusSingleThreshold());
         int[] thresholds = data.getStatusThresholds();
         msg.setFieldInt16(NXCPCodes.VID_STATUS_THRESHOLD_1, thresholds[0]);
         msg.setFieldInt16(NXCPCodes.VID_STATUS_THRESHOLD_2, thresholds[1]);
         msg.setFieldInt16(NXCPCodes.VID_STATUS_THRESHOLD_3, thresholds[2]);
         msg.setFieldInt16(NXCPCodes.VID_STATUS_THRESHOLD_4, thresholds[3]);
      }

      if (data.isFieldSet(NXCObjectModificationData.EXPECTED_STATE))
      {
         msg.setFieldInt16(NXCPCodes.VID_EXPECTED_STATE, data.getExpectedState());
      }

      if (data.isFieldSet(NXCObjectModificationData.LINK_COLOR))
      {
         msg.setFieldInt32(NXCPCodes.VID_LINK_COLOR, data.getLinkColor());
      }

      if (data.isFieldSet(NXCObjectModificationData.CONNECTION_ROUTING))
      {
         msg.setFieldInt16(NXCPCodes.VID_LINK_ROUTING, data.getConnectionRouting());
      }

      if (data.isFieldSet(NXCObjectModificationData.DISCOVERY_RADIUS))
      {
         msg.setFieldInt32(NXCPCodes.VID_DISCOVERY_RADIUS, data.getDiscoveryRadius());
      }

      if (data.isFieldSet(NXCObjectModificationData.HEIGHT))
      {
         msg.setFieldInt16(NXCPCodes.VID_HEIGHT, data.getHeight());
      }

      if (data.isFieldSet(NXCObjectModificationData.RACK_NUMB_SCHEME))
      {
         msg.setField(NXCPCodes.VID_TOP_BOTTOM, data.isRackNumberingTopBottom());
      }

      if (data.isFieldSet(NXCObjectModificationData.PEER_GATEWAY))
      {
         msg.setFieldInt32(NXCPCodes.VID_PEER_GATEWAY, (int)data.getPeerGatewayId());
      }

      if (data.isFieldSet(NXCObjectModificationData.VPN_NETWORKS))
      {
         long fieldId = NXCPCodes.VID_VPN_NETWORK_BASE;         

         msg.setFieldInt32(NXCPCodes.VID_NUM_LOCAL_NETS, data.getLocalNetworks().size());
         for(InetAddressEx a : data.getLocalNetworks())
         {
            msg.setField(fieldId++, a);
         }

         msg.setFieldInt32(NXCPCodes.VID_NUM_REMOTE_NETS, data.getRemoteNetworks().size());
         for(InetAddressEx a : data.getRemoteNetworks())
         {
            msg.setField(fieldId++, a);
         }
      }
      
      if (data.isFieldSet(NXCObjectModificationData.POSTAL_ADDRESS))
      {
         data.getPostalAddress().fillMessage(msg);
      }
      
      if (data.isFieldSet(NXCObjectModificationData.AGENT_CACHE_MODE))
      {
         msg.setFieldInt16(NXCPCodes.VID_AGENT_CACHE_MODE, data.getAgentCacheMode().getValue());
      }
      
      if (data.isFieldSet(NXCObjectModificationData.AGENT_COMPRESSION_MODE))
      {
         msg.setFieldInt16(NXCPCodes.VID_AGENT_COMPRESSION_MODE, data.getAgentCompressionMode().getValue());
      }
      
      if (data.isFieldSet(NXCObjectModificationData.MAPOBJ_DISP_MODE))
      {
         msg.setFieldInt16(NXCPCodes.VID_DISPLAY_MODE, data.getMapObjectDisplayMode().getValue());
      }
      
      if (data.isFieldSet(NXCObjectModificationData.RACK_PLACEMENT))
      {
         msg.setFieldInt32(NXCPCodes.VID_RACK_ID, (int)data.getRackId());
         msg.setField(NXCPCodes.VID_RACK_IMAGE_FRONT, data.getFrontRackImage());
         msg.setField(NXCPCodes.VID_RACK_IMAGE_REAR, data.getRearRackImage());
         msg.setFieldInt16(NXCPCodes.VID_RACK_POSITION, data.getRackPosition());
         msg.setFieldInt16(NXCPCodes.VID_RACK_HEIGHT, data.getRackHeight());
         msg.setFieldInt16(NXCPCodes.VID_RACK_ORIENTATION, data.getRackOrientation().getValue());
      }

      if (data.isFieldSet(NXCObjectModificationData.DASHBOARD_LIST))
      {
         msg.setField(NXCPCodes.VID_DASHBOARDS, data.getDashboards());
      }
      
      if (data.isFieldSet(NXCObjectModificationData.CHASSIS_ID))
      {
         msg.setFieldInt32(NXCPCodes.VID_CHASSIS_ID, (int)data.getChassisId());
      }
      
      if (data.isFieldSet(NXCObjectModificationData.CONTROLLER_ID))
      {
         msg.setFieldInt32(NXCPCodes.VID_CONTROLLER_ID, (int)data.getControllerId());
      }
      
      if (data.isFieldSet(NXCObjectModificationData.SSH_PROXY))
      {
         msg.setFieldInt32(NXCPCodes.VID_SSH_PROXY, (int)data.getSshProxy());
      }
      
      if (data.isFieldSet(NXCObjectModificationData.SSH_LOGIN))
      {
         msg.setField(NXCPCodes.VID_SSH_LOGIN, data.getSshLogin());
      }
      
      if (data.isFieldSet(NXCObjectModificationData.SSH_PASSWORD))
      {
         msg.setField(NXCPCodes.VID_SSH_PASSWORD, data.getSshPassword());
      }

      if (data.isFieldSet(NXCObjectModificationData.ZONE_PROXY))
      {
         msg.setFieldInt32(NXCPCodes.VID_ZONE_PROXY, (int)data.getZoneProxy());
      }
      
      if (data.isFieldSet(NXCObjectModificationData.SEED_OBJECTS))
      {
         msg.setField(NXCPCodes.VID_SEED_OBJECTS, data.getSeedObjectIds());
      }
      
      if (data.isFieldSet(NXCObjectModificationData.PASSIVE_ELEMENTS))
      {
         msg.setField(NXCPCodes.VID_PASSIVE_ELEMENTS, data.getPassiveElements());
      }
            
      modifyCustomObject(data, userData, msg);

      sendMessage(msg);
      waitForRCC(msg.getMessageId());
   }

   /**
    * Modify object (generic interface, in most cases wrapper functions should
    * be used instead). Equivalent of calling modifyObject(data, null).
    *
    * @param data Object modification data
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public void modifyObject(final NXCObjectModificationData data) throws IOException, NXCException
   {
      modifyObject(data, null);
   }

   /**
    * Change object's name (wrapper for modifyObject())
    *
    * @param objectId ID of object to be changed
    * @param name     New object's name
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public void setObjectName(final long objectId, final String name) throws IOException, NXCException
   {
      NXCObjectModificationData data = new NXCObjectModificationData(objectId);
      data.setName(name);
      modifyObject(data);
   }

   /**
    * Change object's custom attributes (wrapper for modifyObject())
    * 
    * @param objectId The object ID
    * @param attrList The attribute list
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public void setObjectCustomAttributes(final long objectId, final Map attrList)
      throws IOException, NXCException
   {
      NXCObjectModificationData data = new NXCObjectModificationData(objectId);
      data.setCustomAttributes(attrList);
      modifyObject(data);
   }

   /**
    * Change object's ACL (wrapper for modifyObject())
    *
    * @param objectId The object id
    * @param acl The AccessListElements
    * @param inheritAccessRights true if access rights should be inherited
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public void setObjectACL(final long objectId, final AccessListElement[] acl, final boolean inheritAccessRights)
      throws IOException, NXCException
   {
      NXCObjectModificationData data = new NXCObjectModificationData(objectId);
      data.setACL(acl);
      data.setInheritAccessRights(inheritAccessRights);
      modifyObject(data);
   }
   
   /**
    * Move object to different zone. Only nodes and clusters can be moved
    * between zones.
    *
    * @param objectId Node or cluster object ID
    * @param zoneUIN The zone UIN (unique identification number)
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public void changeObjectZone(final long objectId, final long zoneUIN) throws IOException, NXCException
   {
      NXCPMessage msg = newMessage(NXCPCodes.CMD_CHANGE_ZONE);
      msg.setFieldInt32(NXCPCodes.VID_OBJECT_ID, (int)objectId);
      msg.setFieldInt32(NXCPCodes.VID_ZONE_UIN, (int)zoneUIN);
      sendMessage(msg);
      waitForRCC(msg.getMessageId());
   }

   /**
    * Change object's comments.
    *
    * @param objectId Object's ID
    * @param comments New comments
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public void updateObjectComments(final long objectId, final String comments) throws IOException, NXCException
   {
      NXCPMessage msg = newMessage(NXCPCodes.CMD_UPDATE_OBJECT_COMMENTS);
      msg.setFieldInt32(NXCPCodes.VID_OBJECT_ID, (int) objectId);
      msg.setField(NXCPCodes.VID_COMMENTS, comments);
      sendMessage(msg);
      waitForRCC(msg.getMessageId());
   }

   /**
    * Set object's managed status.
    *
    * @param objectId  object's identifier
    * @param isManaged object's managed status
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public void setObjectManaged(final long objectId, final boolean isManaged) throws IOException, NXCException
   {
      NXCPMessage msg = newMessage(NXCPCodes.CMD_SET_OBJECT_MGMT_STATUS);
      msg.setFieldInt32(NXCPCodes.VID_OBJECT_ID, (int) objectId);
      msg.setFieldInt16(NXCPCodes.VID_MGMT_STATUS, isManaged ? 1 : 0);
      sendMessage(msg);
      waitForRCC(msg.getMessageId());
   }

   /**
    * Get effective rights of currently logged in user to given object.
    * 
    * @param objectId The object ID
    * @return The effective rights
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public int getEffectiveRights(final long objectId) throws IOException, NXCException
   {
      NXCPMessage msg = newMessage(NXCPCodes.CMD_GET_EFFECTIVE_RIGHTS);
      msg.setFieldInt32(NXCPCodes.VID_OBJECT_ID, (int) objectId);
      sendMessage(msg);
      return waitForRCC(msg.getMessageId()).getFieldAsInt32(NXCPCodes.VID_EFFECTIVE_RIGHTS);
   }

   /**
    * Common internal implementation for bindObject, unbindObject, and
    * removeTemplate
    *
    * @param parentId  parent object's identifier
    * @param childId   Child object's identifier
    * @param bind      true if operation is "bind"
    * @param removeDci true if DCIs created from template should be removed during
    *                  unbind
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   private void changeObjectBinding(long parentId, long childId, boolean bind, boolean removeDci)
      throws IOException, NXCException
   {
      NXCPMessage msg = newMessage(bind ? NXCPCodes.CMD_BIND_OBJECT : NXCPCodes.CMD_UNBIND_OBJECT);
      msg.setFieldInt32(NXCPCodes.VID_PARENT_ID, (int) parentId);
      msg.setFieldInt32(NXCPCodes.VID_CHILD_ID, (int) childId);
      msg.setFieldInt16(NXCPCodes.VID_REMOVE_DCI, removeDci ? 1 : 0);
      sendMessage(msg);
      waitForRCC(msg.getMessageId());
   }

   /**
    * Bind object.
    *
    * @param parentId parent object's identifier
    * @param childId  Child object's identifier
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public void bindObject(final long parentId, final long childId) throws IOException, NXCException
   {
      changeObjectBinding(parentId, childId, true, false);
   }

   /**
    * Unbind object.
    *
    * @param parentId parent object's identifier
    * @param childId  Child object's identifier
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public void unbindObject(final long parentId, final long childId) throws IOException, NXCException
   {
      changeObjectBinding(parentId, childId, false, false);
   }

   /**
    * Remove data collection template from node.
    *
    * @param templateId template object identifier
    * @param nodeId     node object identifier
    * @param removeDci  true if DCIs created from this template should be removed
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public void removeTemplate(final long templateId, final long nodeId, final boolean removeDci)
      throws IOException, NXCException
   {
      changeObjectBinding(templateId, nodeId, false, removeDci);
   }

   /**
    * Apply data collection template to node.
    *
    * @param templateId template object ID
    * @param nodeId     node object ID
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public void applyTemplate(long templateId, long nodeId) throws IOException, NXCException
   {
      NXCPMessage msg = newMessage(NXCPCodes.CMD_APPLY_TEMPLATE);
      msg.setFieldInt32(NXCPCodes.VID_SOURCE_OBJECT_ID, (int) templateId);
      msg.setFieldInt32(NXCPCodes.VID_DESTINATION_OBJECT_ID, (int) nodeId);
      sendMessage(msg);
      waitForRCC(msg.getMessageId());
   }

   /**
    * Add node to cluster.
    *
    * @param clusterId cluster object ID
    * @param nodeId    node object ID
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public void addClusterNode(final long clusterId, final long nodeId) throws IOException, NXCException
   {
      NXCPMessage msg = newMessage(NXCPCodes.CMD_ADD_CLUSTER_NODE);
      msg.setFieldInt32(NXCPCodes.VID_PARENT_ID, (int) clusterId);
      msg.setFieldInt32(NXCPCodes.VID_CHILD_ID, (int) nodeId);
      sendMessage(msg);
      waitForRCC(msg.getMessageId());
   }

   /**
    * Remove node from cluster.
    *
    * @param clusterId cluster object ID
    * @param nodeId    node object ID
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public void removeClusterNode(final long clusterId, final long nodeId) throws IOException, NXCException
   {
      changeObjectBinding(clusterId, nodeId, false, true);
   }

   /**
    * Query layer 2 topology for node
    *
    * @param nodeId The node ID
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    * @return The NetworkMapPage
    */
   public NetworkMapPage queryLayer2Topology(final long nodeId) throws IOException, NXCException
   {
      NXCPMessage msg = newMessage(NXCPCodes.CMD_QUERY_L2_TOPOLOGY);
      msg.setFieldInt32(NXCPCodes.VID_OBJECT_ID, (int) nodeId);
      sendMessage(msg);

      final NXCPMessage response = waitForRCC(msg.getMessageId());

      int count = response.getFieldAsInt32(NXCPCodes.VID_NUM_OBJECTS);
      long[] idList = response.getFieldAsUInt32Array(NXCPCodes.VID_OBJECT_LIST);
      if (idList.length != count) 
         throw new NXCException(RCC.INTERNAL_ERROR);

      NetworkMapPage page = new NetworkMapPage(msg.getMessageId() + ".L2Topology");
      for(int i = 0; i < count; i++)
      {
         page.addElement(new NetworkMapObject(page.createElementId(), idList[i]));
      }

      count = response.getFieldAsInt32(NXCPCodes.VID_NUM_LINKS);
      long varId = NXCPCodes.VID_OBJECT_LINKS_BASE;
      for(int i = 0; i < count; i++, varId += 4)
      {
         NetworkMapObject obj1 = page.findObjectElement(response.getFieldAsInt64(varId++));
         NetworkMapObject obj2 = page.findObjectElement(response.getFieldAsInt64(varId++));
         int type = response.getFieldAsInt32(varId++);
         String port1 = response.getFieldAsString(varId++);
         String port2 = response.getFieldAsString(varId++);
         int flags = response.getFieldAsInt32(varId++);
         if ((obj1 != null) && (obj2 != null))
         {
            page.addLink(new NetworkMapLink("", type, obj1.getId(), obj2.getId(), port1, port2, flags));
         }
      }
      return page;
   }

   /**
    * Execute action on remote agent
    *
    * @param nodeId Node object ID
    * @param action Action name
    * @param args Action arguments
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public void executeAction(long nodeId, String action, String[] args) throws IOException, NXCException
   {
      executeAction(nodeId, action, args, false, null, null);
   }

   /**
    * Execute action on remote agent
    *
    * @param nodeId Node object ID
    * @param action Action name
    * @param args Action arguments
    * @param receiveOutput true if action's output has to be read
    * @param listener listener for action's output or null
    * @param writer writer for action's output or null
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public void executeAction(long nodeId, String action, String[] args, boolean receiveOutput, final TextOutputListener listener, final Writer writer) throws IOException, NXCException
   {
      NXCPMessage msg = newMessage(NXCPCodes.CMD_EXECUTE_ACTION);
      msg.setFieldInt32(NXCPCodes.VID_OBJECT_ID, (int) nodeId);
      msg.setField(NXCPCodes.VID_ACTION_NAME, action);
      msg.setField(NXCPCodes.VID_RECEIVE_OUTPUT, receiveOutput);
      
      if (args != null)
      {
         msg.setFieldInt16(NXCPCodes.VID_NUM_ARGS, args.length);
         long fieldId = NXCPCodes.VID_ACTION_ARG_BASE;
         for(String a : args)
            msg.setField(fieldId++, a);
      }
      else
      {
         msg.setFieldInt16(NXCPCodes.VID_NUM_ARGS, 0);
      }
      
      MessageHandler handler = receiveOutput ? new MessageHandler() {
         @Override
         public boolean processMessage(NXCPMessage m)
         {
            String text = m.getFieldAsString(NXCPCodes.VID_MESSAGE);
            if (text != null)
            {
               if (listener != null)
                  listener.messageReceived(text);
               if (writer != null)
               {
                  try
                  {
                     writer.write(text);
                  }
                  catch(IOException e)
                  {
                  }
               }
            }
            if (m.isEndOfSequence())
               setComplete();
            return true;
         }
      } : null;
      if (receiveOutput)
         addMessageSubscription(NXCPCodes.CMD_COMMAND_OUTPUT, msg.getMessageId(), handler);
      
      sendMessage(msg);
      waitForRCC(msg.getMessageId());

      if (receiveOutput)
      {
         synchronized(handler)
         {
            try
            {
               handler.wait();
            }
            catch(InterruptedException e)
            {
            }
         }
         if (handler.isTimeout())
            throw new NXCException(RCC.TIMEOUT);
      }      
   }
   
   /**
    * Wakeup node by sending wake-on-LAN magic packet. Either node ID or
    * interface ID may be given. If node ID is given, system will send wakeup
    * packets to all active interfaces with IP address.
    *
    * @param objectId node or interface ID
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public void wakeupNode(final long objectId) throws IOException, NXCException
   {
      NXCPMessage msg = newMessage(NXCPCodes.CMD_WAKEUP_NODE);
      msg.setFieldInt32(NXCPCodes.VID_OBJECT_ID, (int) objectId);
      sendMessage(msg);
      waitForRCC(msg.getMessageId());
   }

   /**
    * Get node's physical components (obtained from ENTITY-MIB).
    *
    * @param nodeId node object identifier
    * @return root component
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public PhysicalComponent getNodePhysicalComponents(long nodeId) throws IOException, NXCException
   {
      final NXCPMessage msg = newMessage(NXCPCodes.CMD_GET_NODE_COMPONENTS);
      msg.setFieldInt32(NXCPCodes.VID_OBJECT_ID, (int) nodeId);
      sendMessage(msg);
      final NXCPMessage response = waitForRCC(msg.getMessageId());
      return new PhysicalComponent(response, NXCPCodes.VID_COMPONENT_LIST_BASE, null);
   }

   /**
    * Get list of available Windows performance objects. Returns empty list if node
    * does is not a Windows node or does not have WinPerf subagent installed.
    *
    * @param nodeId node object ID
    * @return list of available Windows performance objects
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public List getNodeWinPerfObjects(long nodeId) throws IOException, NXCException
   {
      final NXCPMessage msg = newMessage(NXCPCodes.CMD_GET_WINPERF_OBJECTS);
      msg.setFieldInt32(NXCPCodes.VID_OBJECT_ID, (int) nodeId);
      sendMessage(msg);
      final NXCPMessage response = waitForRCC(msg.getMessageId());
      return WinPerfObject.createListFromMessage(response);
   }

   /**
    * Get list of software packages installed on node.
    *
    * @param nodeId node object identifier
    * @return root component
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public List getNodeSoftwarePackages(long nodeId) throws IOException, NXCException
   {
      final NXCPMessage msg = newMessage(NXCPCodes.CMD_GET_NODE_SOFTWARE);
      msg.setFieldInt32(NXCPCodes.VID_OBJECT_ID, (int) nodeId);
      sendMessage(msg);
      final NXCPMessage response = waitForRCC(msg.getMessageId());
      int count = response.getFieldAsInt32(NXCPCodes.VID_NUM_ELEMENTS);
      List packages = new ArrayList(count);
      long varId = NXCPCodes.VID_ELEMENT_LIST_BASE;
      for(int i = 0; i < count; i++)
      {
         packages.add(new SoftwarePackage(response, varId));
         varId += 10;
      }
      return packages;
   }

   /**
    * Get list of server jobs
    *
    * @return list of server jobs
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public ServerJob[] getServerJobList() throws IOException, NXCException
   {
      NXCPMessage msg = newMessage(NXCPCodes.CMD_GET_JOB_LIST);
      sendMessage(msg);

      final NXCPMessage response = waitForRCC(msg.getMessageId());

      int count = response.getFieldAsInt32(NXCPCodes.VID_JOB_COUNT);
      ServerJob[] jobList = new ServerJob[count];
      long baseVarId = NXCPCodes.VID_JOB_LIST_BASE;
      for(int i = 0; i < count; i++, baseVarId += 10)
      {
         jobList[i] = new ServerJob(response, baseVarId);
      }

      return jobList;
   }

   /**
    * Cancel server job
    *
    * @param jobId Job ID
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public void cancelServerJob(long jobId) throws IOException, NXCException
   {
      NXCPMessage msg = newMessage(NXCPCodes.CMD_CANCEL_JOB);
      msg.setFieldInt32(NXCPCodes.VID_JOB_ID, (int) jobId);
      sendMessage(msg);
      waitForRCC(msg.getMessageId());
   }

   /**
    * Put server job on hold
    *
    * @param jobId Job ID
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public void holdServerJob(long jobId) throws IOException, NXCException
   {
      NXCPMessage msg = newMessage(NXCPCodes.CMD_HOLD_JOB);
      msg.setFieldInt32(NXCPCodes.VID_JOB_ID, (int) jobId);
      sendMessage(msg);
      waitForRCC(msg.getMessageId());
   }

   /**
    * Put server on hold job to pending state
    *
    * @param jobId Job ID
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public void unholdServerJob(long jobId) throws IOException, NXCException
   {
      NXCPMessage msg = newMessage(NXCPCodes.CMD_UNHOLD_JOB);
      msg.setFieldInt32(NXCPCodes.VID_JOB_ID, (int) jobId);
      sendMessage(msg);
      waitForRCC(msg.getMessageId());
   }

   /**
    * Deploy policy on agent
    *
    * @param policyId Policy object ID
    * @param nodeId   Node object ID
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public void deployAgentPolicy(final long policyId, final long nodeId) throws IOException, NXCException
   {
      NXCPMessage msg = newMessage(NXCPCodes.CMD_DEPLOY_AGENT_POLICY);
      msg.setFieldInt32(NXCPCodes.VID_POLICY_ID, (int) policyId);
      msg.setFieldInt32(NXCPCodes.VID_OBJECT_ID, (int) nodeId);
      sendMessage(msg);
      waitForRCC(msg.getMessageId());
   }

   /**
    * Uninstall policy from agent
    *
    * @param policyId Policy object ID
    * @param nodeId   Node object ID
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public void uninstallAgentPolicy(final long policyId, final long nodeId) throws IOException, NXCException
   {
      NXCPMessage msg = newMessage(NXCPCodes.CMD_UNINSTALL_AGENT_POLICY);
      msg.setFieldInt32(NXCPCodes.VID_POLICY_ID, (int) policyId);
      msg.setFieldInt32(NXCPCodes.VID_OBJECT_ID, (int) nodeId);
      sendMessage(msg);
      waitForRCC(msg.getMessageId());
   }

   /**
    * Internal implementation for open/get event processing policy.
    *
    * @param readOnly true to get read-only copy of the policy
    * @return Event processing policy
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   private EventProcessingPolicy getEventProcessingPolicyInternal(boolean readOnly) throws IOException, NXCException
   {
      NXCPMessage msg = newMessage(NXCPCodes.CMD_OPEN_EPP);
      msg.setFieldInt16(NXCPCodes.VID_READ_ONLY, readOnly ? 1 : 0);
      sendMessage(msg);
      NXCPMessage response = waitForRCC(msg.getMessageId());

      int numRules = response.getFieldAsInt32(NXCPCodes.VID_NUM_RULES);
      final EventProcessingPolicy policy = new EventProcessingPolicy(numRules);

      for(int i = 0; i < numRules; i++)
      {
         response = waitForMessage(NXCPCodes.CMD_EPP_RECORD, msg.getMessageId());
         policy.addRule(new EventProcessingPolicyRule(response, i+1));
      }

      return policy;
   }

   /**
    * Get read-only copy of event processing policy.
    *
    * @return Event processing policy
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public EventProcessingPolicy getEventProcessingPolicy() throws IOException, NXCException
   {
      return getEventProcessingPolicyInternal(true);
   }

   /**
    * Open event processing policy for editing. This call will lock event
    * processing policy on server until closeEventProcessingPolicy called or
    * session terminated.
    *
    * @return Event processing policy
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public EventProcessingPolicy openEventProcessingPolicy() throws IOException, NXCException
   {
      return getEventProcessingPolicyInternal(false);
   }

   /**
    * Save event processing policy. If policy was not previously open by current
    * session exception will be thrown.
    *
    * @param epp Modified event processing policy
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public void saveEventProcessingPolicy(EventProcessingPolicy epp) throws IOException, NXCException
   {
      final List rules = epp.getRules();

      NXCPMessage msg = newMessage(NXCPCodes.CMD_SAVE_EPP);
      msg.setFieldInt32(NXCPCodes.VID_NUM_RULES, rules.size());
      sendMessage(msg);
      final long msgId = msg.getMessageId();
      waitForRCC(msgId);

      int id = 1;
      for(EventProcessingPolicyRule rule : rules)
      {
         msg = new NXCPMessage(NXCPCodes.CMD_EPP_RECORD);
         msg.setMessageId(msgId);
         msg.setFieldInt32(NXCPCodes.VID_RULE_ID, id++);
         rule.fillMessage(msg);
         sendMessage(msg);
      }

      // Wait for final confirmation if there was some rules
      if (rules.size() > 0) waitForRCC(msgId);
   }

   /**
    * Close event processing policy. This call will unlock event processing
    * policy on server. If policy was not previously open by current session
    * exception will be thrown.
    *
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public void closeEventProcessingPolicy() throws IOException, NXCException
   {
      NXCPMessage msg = newMessage(NXCPCodes.CMD_CLOSE_EPP);
      sendMessage(msg);
      waitForRCC(msg.getMessageId());
   }

   /**
    * Open data collection configuration for given node. You must call
    * DataCollectionConfiguration.close() to close data collection configuration
    * when it is no longer needed.
    *
    * @param nodeId Node object identifier
    * @return Data collection configuration object
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public DataCollectionConfiguration openDataCollectionConfiguration(long nodeId) throws IOException, NXCException
   {
      final DataCollectionConfiguration cfg = new DataCollectionConfiguration(this, nodeId);
      cfg.open();
      return cfg;
   }
   
   /**
    * Clear data collection configuration on agent. Will wipe out all configuration and collected data.
    *
    * @param nodeId node object ID
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public void clearAgentDataCollectionConfiguration(final long nodeId) throws IOException, NXCException
   {
      NXCPMessage msg = newMessage(NXCPCodes.CMD_CLEAN_AGENT_DCI_CONF);
      msg.setFieldInt32(NXCPCodes.VID_NODE_ID, (int) nodeId);
      sendMessage(msg);
      waitForRCC(msg.getMessageId());
   }
   

   /**
    * Force re-synchronization of data collection configuration with agent.
    *
    * @param nodeId node object ID
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public void resyncAgentDataCollectionConfiguration(final long nodeId) throws IOException, NXCException
   {
      NXCPMessage msg = newMessage(NXCPCodes.CMD_RESYNC_AGENT_DCI_CONF);
      msg.setFieldInt32(NXCPCodes.VID_NODE_ID, (int) nodeId);
      sendMessage(msg);
      waitForRCC(msg.getMessageId());
   }
   
   /**
    * Test DCI transformation script.
    *
    * @param nodeId     ID of the node object to test script on
    * @param script     script source code
    * @param inputValue input value for the script
    * @return test execution results
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public TransformationTestResult testTransformationScript(long nodeId, String script, String inputValue)
      throws IOException, NXCException
   {
      NXCPMessage msg = newMessage(NXCPCodes.CMD_TEST_DCI_TRANSFORMATION);
      msg.setFieldInt32(NXCPCodes.VID_OBJECT_ID, (int) nodeId);
      msg.setField(NXCPCodes.VID_SCRIPT, script);
      msg.setField(NXCPCodes.VID_VALUE, inputValue);
      sendMessage(msg);
      final NXCPMessage response = waitForRCC(msg.getMessageId());
      TransformationTestResult r = new TransformationTestResult();
      r.success = response.getFieldAsBoolean(NXCPCodes.VID_EXECUTION_STATUS);
      r.result = response.getFieldAsString(NXCPCodes.VID_EXECUTION_RESULT);
      return r;
   }
   
   /**
    * Process server script execution.
    * 
    * @param msg prepared request message
    * @param listener script output listener or null if caller not interested in script output
    * @throws IOException if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   private void processScriptExecution(NXCPMessage msg, final TextOutputListener listener) throws IOException, NXCException
   {
      MessageHandler handler = null;
      if (listener != null)
      {
         handler = new MessageHandler() {
            @Override
            public boolean processMessage(NXCPMessage m)
            {
               if (m.getFieldAsInt32(NXCPCodes.VID_RCC) != RCC.SUCCESS)
               {
                  String errorMessage = m.getFieldAsString(NXCPCodes.VID_ERROR_TEXT);
                  if ((errorMessage != null) && (listener != null))
                  {
                     listener.messageReceived(errorMessage + "\n\n");
                  }
               }
               
               String text = m.getFieldAsString(NXCPCodes.VID_MESSAGE);
               if ((text != null) && (listener != null))
               {
                  listener.messageReceived(text);
               }
               
               if (m.isEndOfSequence())
                  setComplete();
               return true;
            }
         };
         addMessageSubscription(NXCPCodes.CMD_EXECUTE_SCRIPT_UPDATE, msg.getMessageId(), handler);
      }
      sendMessage(msg);
      waitForRCC(msg.getMessageId());
      if (listener != null)
      {
         synchronized(handler)
         {
            if (!handler.isComplete())
            {
               try
               {
                  handler.wait();
               }
               catch(InterruptedException e)
               {
               }
            }
         }
         if (handler.isTimeout())
            throw new NXCException(RCC.TIMEOUT);
      }
   }
   
   /**
    * Execute library script on object. Script name interpreted as command line with server-side macro substitution. Map inputValues
    * can be used to pass data for %() macros.
    * 
    * @param nodeId node ID to execute script on
    * @param script script name and parameters
    * @param inputFields input values map for %() macro substitution (can be null)
    * @param listener script output listener
    * @throws IOException if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public void executeLibraryScript(long nodeId, String script, Map inputFields, final TextOutputListener listener) throws IOException, NXCException
   {
      NXCPMessage msg = newMessage(NXCPCodes.CMD_EXECUTE_LIBRARY_SCRIPT);
      msg.setFieldInt32(NXCPCodes.VID_OBJECT_ID, (int)nodeId);
      msg.setField(NXCPCodes.VID_SCRIPT, script);
      msg.setField(NXCPCodes.VID_RECEIVE_OUTPUT, listener != null);
      if (inputFields != null)
      {
         msg.setFieldInt16(NXCPCodes.VID_NUM_FIELDS, inputFields.size());
         long fieldId = NXCPCodes.VID_FIELD_LIST_BASE;
         for(Entry e : inputFields.entrySet())
         {
            msg.setField(fieldId++, e.getKey());
            msg.setField(fieldId++, e.getValue());
         }
      }
      processScriptExecution(msg, listener);
   }
   
   /**
    * Execute script.
    *
    * @param nodeId ID of the node object to test script on
    * @param script script source code
    * @param listener script output listener
    * @throws IOException if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public void executeScript(long nodeId, String script, final TextOutputListener listener) throws IOException, NXCException
   {
      NXCPMessage msg = newMessage(NXCPCodes.CMD_EXECUTE_SCRIPT);
      msg.setFieldInt32(NXCPCodes.VID_OBJECT_ID, (int)nodeId);
      msg.setField(NXCPCodes.VID_SCRIPT, script);
      processScriptExecution(msg, listener);
   }

   /**
    * Compile NXSL script on server. Field *success* in compilation result object will indicate compilation status.
    * If compilation fails, field *errorMessage* will contain compilation error message.
    * 
    * @param source script source
    * @param serialize flag to indicate if compiled script should be serialized and sent back to client
    * @return script compilation result object
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public ScriptCompilationResult compileScript(String source, boolean serialize) throws IOException, NXCException
   {
      NXCPMessage msg = newMessage(NXCPCodes.CMD_COMPILE_SCRIPT);
      msg.setField(NXCPCodes.VID_SCRIPT, source);
      msg.setField(NXCPCodes.VID_SERIALIZE, serialize);
      sendMessage(msg);
      return new ScriptCompilationResult(waitForRCC(msg.getMessageId()));
   }

   /**
    * Open server log by name.
    *
    * @param logName Log name
    * @return Log object
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public Log openServerLog(final String logName) throws IOException, NXCException
   {
      NXCPMessage msg = newMessage(NXCPCodes.CMD_OPEN_SERVER_LOG);
      msg.setField(NXCPCodes.VID_LOG_NAME, logName);
      sendMessage(msg);
      final NXCPMessage response = waitForRCC(msg.getMessageId());
      Log log = new Log(this, response, logName);
      return log;
   }

   /**
    * Get alarm categories from server
    * 
    * @return List of configured alarm categories
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public List getAlarmCategories() throws IOException, NXCException
   {
      NXCPMessage msg = newMessage(NXCPCodes.CMD_GET_ALARM_CATEGORIES);
      sendMessage(msg);
      final NXCPMessage response = waitForRCC(msg.getMessageId());
      ArrayList list = new ArrayList();
      long fieldId = NXCPCodes.VID_ELEMENT_LIST_BASE;
      int count = response.getFieldAsInt32(NXCPCodes.VID_NUM_ELEMENTS);
      for(int i = 0; i < count; i++)
      {
         list.add(new AlarmCategory(response, fieldId));
         fieldId += 10;
      }
      return list;
   }
   
   /**
    * Add or update alarm category in DB
    * 
    * @param object alarm category
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    * @return The ID of the category
    */
   public long modifyAlarmCategory(AlarmCategory object) throws IOException, NXCException
   {
      if (object.getName().isEmpty())
         return 0;
      NXCPMessage msg = newMessage(NXCPCodes.CMD_MODIFY_ALARM_CATEGORY);
      object.fillMessage(msg);
      sendMessage(msg);
      NXCPMessage response = waitForRCC(msg.getMessageId());
      return response.getFieldAsInt32(NXCPCodes.VID_CATEGORY_ID);
   }
   
   /**
    * Delete alarm category in DB
    * @param id of alarm category
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public void deleteAlarmCategory(long id) throws IOException, NXCException
   {
      NXCPMessage msg = newMessage(NXCPCodes.CMD_DELETE_ALARM_CATEGORY);
      msg.setFieldInt32(NXCPCodes.VID_CATEGORY_ID, (int)id);
      sendMessage(msg);
      waitForRCC(msg.getMessageId());
   }
   
   /**
    * Synchronize alarm category configuration. After call to this method
    * session object will maintain internal list of configured alarm categories
    * 
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   
   public void syncAlarmCategories() throws IOException, NXCException
   {
      List categories = getAlarmCategories();
      synchronized(alarmCategories)
      {
         alarmCategories.clear();
         for(AlarmCategory c : categories)
         {
            alarmCategories.put(c.getId(), c);
         }
         alarmCategoriesNeedSync = true;
      }
   }
   
   /**
    * Find alarm category by id in alarm category database internally
    * maintained by session object. You must call
    * NXCSession.syncAlarmCategories() first to make local copy of event template
    * database.
    *
    * @param id alarm category id
    * @return Event template object or null if not found
    */
   public AlarmCategory findAlarmCategoryById(long id)
   {
      synchronized(alarmCategories)
      {
         return alarmCategories.get(id);
      }
   }
   
   /**
    * Find alarm category by name in alarm category database internally
    * maintained by session object. You must call
    * NXCSession.syncAlarmCategories() first to make local copy of event template
    * database.
    *
    * @param name alarm category name
    * @return alarm category with given name or null if not found
    */
   public AlarmCategory findAlarmCategoryByName(String name)
   {
      synchronized(alarmCategories)
      {
         for (Map.Entry c : alarmCategories.entrySet())
         {
            if (((AlarmCategory)c.getValue()).getName().equals(name))
               return (AlarmCategory)c.getValue();
         }
         return null;
      }
   }
   
   /**
    * Find multiple alarm categories by category id`s in alarm category database
    * internally maintained by session object. You must call
    * NXCSession.syncAlarmCategories() first to make local copy of alarm category
    * database.
    *
    * @param ids List of alarm category id`s
    * @return List of found alarm categories
    */
   public List findMultipleAlarmCategories(List ids)
   {
      List list = new ArrayList();
      synchronized(alarmCategories)
      {
         for(Long id : ids)
         {
            AlarmCategory e = alarmCategories.get(id);
            if (e != null) list.add(e);
         }
      }
      return list;
   }
   
   /**
    * Synchronize event templates configuration. After call to this method
    * session object will maintain internal list of configured event templates.
    *
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public void syncEventTemplates() throws IOException, NXCException
   {
      List templates = getEventTemplates();
      synchronized(eventTemplates)
      {
         eventTemplates.clear();
         for(EventTemplate t : templates)
         {
            eventTemplates.put(t.getCode(), t);
         }
         eventTemplatesNeedSync = true;
      }
   }
   
   /**
    * Re-synchronize event templaytes in background
    */
   private void resyncEventTemplates()
   {
      new Thread(new Runnable() {
         @Override
         public void run()
         {
            try
            {
               syncEventTemplates();
            }
            catch(Exception e)
            {
               Logger.error("NXCSession.resyncEventTemplates", "Exception in worker thread", e);
            }
         }
      }).start();
   }

   /**
    * Get cached list event templates
    *
    * @return List of event templates cached by client library
    */
   public EventTemplate[] getCachedEventTemplates()
   {
      EventTemplate[] events = null;
      synchronized(eventTemplates)
      {
         events = eventTemplates.values().toArray(new EventTemplate[eventTemplates.size()]);
      }
      return events;
   }

   /**
    * Find event template by name in event template database internally
    * maintained by session object. You must call
    * NXCSession.syncEventTemplates() first to make local copy of event template
    * database.
    *
    * @param name Event name
    * @return Event template object or null if not found
    */
   public EventTemplate findEventTemplateByName(String name)
   {
      EventTemplate result = null;
      synchronized(eventTemplates)
      {
         for(EventTemplate e : eventTemplates.values())
         {
            if (e.getName().equalsIgnoreCase(name))
            {
               result = e;
               break;
            }
         }
      }
      return result;
   }
   
   /**
    * Get event name from event code
    * 
    * @param code event code
    * @return event name or event code as string if event not found
    */
   public String getEventName(long code)
   {
      synchronized(eventTemplates)
      {
         EventTemplate e = eventTemplates.get(code);
         return (e != null) ? e.getName() : ("[" + Long.toString(code) + "]");
      }
   }

   /**
    * Find event template by code in event template database internally
    * maintained by session object. You must call
    * NXCSession.syncEventTemplates() first to make local copy of event template
    * database.
    *
    * @param code Event code
    * @return Event template object or null if not found
    */
   public EventTemplate findEventTemplateByCode(long code)
   {
      synchronized(eventTemplates)
      {
         return eventTemplates.get(code);
      }
   }

   /**
    * Find multiple event templates by event codes in event template database
    * internally maintained by session object. You must call
    * NXCSession.syncEventTemplates() first to make local copy of event template
    * database.
    *
    * @param codes List of event codes
    * @return List of found event templates
    */
   public List findMultipleEventTemplates(final Long[] codes)
   {
      List list = new ArrayList();
      synchronized(eventTemplates)
      {
         for(long code : codes)
         {
            EventTemplate e = eventTemplates.get(code);
            if (e != null) list.add(e);
         }
      }
      return list;
   }

   /**
    * Find multiple event templates by event codes in event template database
    * internally maintained by session object. You must call
    * NXCSession.syncEventTemplates() first to make local copy of event template
    * database.
    *
    * @param codes List of event codes
    * @return List of found event templates
    */
   public List findMultipleEventTemplates(final long[] codes)
   {
      List list = new ArrayList();
      synchronized(eventTemplates)
      {
         for(long code : codes)
         {
            EventTemplate e = eventTemplates.get(code);
            if (e != null) list.add(e);
         }
      }
      return list;
   }

   /**
    * Get event templates from server
    *
    * @return List of configured event templates
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public List getEventTemplates() throws IOException, NXCException
   {
      NXCPMessage msg = newMessage(NXCPCodes.CMD_LOAD_EVENT_DB);
      sendMessage(msg);
      waitForRCC(msg.getMessageId());
      ArrayList list = new ArrayList();
      while(true)
      {
         final NXCPMessage response = waitForMessage(NXCPCodes.CMD_EVENT_DB_RECORD, msg.getMessageId());
         if (response.isEndOfSequence()) 
            break;
         list.add(new EventTemplate(response));
      }
      return list;
   }

   /**
    * Generate code for new event template.
    *
    * @return Code for new event template
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public long generateEventCode() throws IOException, NXCException
   {
      final NXCPMessage msg = newMessage(NXCPCodes.CMD_GENERATE_EVENT_CODE);
      sendMessage(msg);
      final NXCPMessage response = waitForRCC(msg.getMessageId());
      return response.getFieldAsInt64(NXCPCodes.VID_EVENT_CODE);
   }

   /**
    * Delete event template.
    *
    * @param eventCode Event code
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public void deleteEventTemplate(long eventCode) throws IOException, NXCException
   {
      final NXCPMessage msg = newMessage(NXCPCodes.CMD_DELETE_EVENT_TEMPLATE);
      msg.setFieldInt32(NXCPCodes.VID_EVENT_CODE, (int) eventCode);
      sendMessage(msg);
      waitForRCC(msg.getMessageId());
   }

   /**
    * Modify event template.
    *
    * @param evt Event template
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public void modifyEventTemplate(EventTemplate evt) throws IOException, NXCException
   {
      final NXCPMessage msg = newMessage(NXCPCodes.CMD_SET_EVENT_INFO);
      msg.setFieldInt32(NXCPCodes.VID_EVENT_CODE, (int) evt.getCode());
      msg.setFieldInt32(NXCPCodes.VID_SEVERITY, evt.getSeverity().getValue());
      msg.setFieldInt32(NXCPCodes.VID_FLAGS, evt.getFlags());
      msg.setField(NXCPCodes.VID_NAME, evt.getName());
      msg.setField(NXCPCodes.VID_MESSAGE, evt.getMessage());
      msg.setField(NXCPCodes.VID_DESCRIPTION, evt.getDescription());
      sendMessage(msg);
      waitForRCC(msg.getMessageId());
   }

   /**
    * Send event to server. Event can be identified either by event code or event name. If event name
    * is given, event code will be ignored.
    *
    * Node: sending events by name supported by server version 1.1.8 and higher.
    *
    * @param eventCode  event code. Ignored if event name is not null.
    * @param eventName  event name. Must be set to null if event identified by code.
    * @param objectId   Object ID to send event on behalf of. If set to 0, server will determine object ID by client IP address.
    * @param parameters event's parameters
    * @param userTag    event's user tag
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public void sendEvent(long eventCode, String eventName, long objectId, String[] parameters, String userTag)
      throws IOException, NXCException
   {
      final NXCPMessage msg = newMessage(NXCPCodes.CMD_TRAP);
      msg.setFieldInt32(NXCPCodes.VID_EVENT_CODE, (int) eventCode);
      if (eventName != null) msg.setField(NXCPCodes.VID_EVENT_NAME, eventName);
      msg.setFieldInt32(NXCPCodes.VID_OBJECT_ID, (int) objectId);
      msg.setField(NXCPCodes.VID_USER_TAG, (userTag != null) ? userTag : "");
      msg.setFieldInt16(NXCPCodes.VID_NUM_ARGS, parameters.length);
      long varId = NXCPCodes.VID_EVENT_ARG_BASE;
      for(int i = 0; i < parameters.length; i++)
      {
         msg.setField(varId++, parameters[i]);
      }
      sendMessage(msg);
      waitForRCC(msg.getMessageId());
   }

   /**
    * Convenience wrapper for sendEvent interface.
    *
    * @param eventCode  event code
    * @param parameters event's parameters
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public void sendEvent(long eventCode, String[] parameters) throws IOException, NXCException
   {
      sendEvent(eventCode, null, 0, parameters, null);
   }

   /**
    * Convenience wrapper for sendEvent interface.
    *
    * @param eventName  event name
    * @param parameters event's parameters
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public void sendEvent(String eventName, String[] parameters) throws IOException, NXCException
   {
      sendEvent(0, eventName, 0, parameters, null);
   }

   /**
    * Get list of well-known SNMP communities configured on server.
    *
    * @return List of SNMP community strings
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public List getSnmpCommunities() throws IOException, NXCException
   {
      final NXCPMessage msg = newMessage(NXCPCodes.CMD_GET_COMMUNITY_LIST);
      sendMessage(msg);
      final NXCPMessage response = waitForRCC(msg.getMessageId());

      int count = response.getFieldAsInt32(NXCPCodes.VID_NUM_STRINGS);
      ArrayList list = new ArrayList(count);
      long varId = NXCPCodes.VID_STRING_LIST_BASE;
      for(int i = 0; i < count; i++)
      {
         list.add(response.getFieldAsString(varId++));
      }

      return list;
   }

   /**
    * Update list of well-known SNMP community strings on server. Existing list
    * will be replaced by given one.
    *
    * @param list New list of SNMP community strings
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public void updateSnmpCommunities(final List list) throws IOException, NXCException
   {
      final NXCPMessage msg = newMessage(NXCPCodes.CMD_UPDATE_COMMUNITY_LIST);

      msg.setFieldInt32(NXCPCodes.VID_NUM_STRINGS, list.size());
      long varId = NXCPCodes.VID_STRING_LIST_BASE;
      for(int i = 0; i < list.size(); i++)
      {
         msg.setField(varId++, list.get(i));
      }

      sendMessage(msg);
      waitForRCC(msg.getMessageId());
   }

   /**
    * Get list of well-known SNMP USM (user security model) credentials
    * configured on server.
    *
    * @return List of SNMP USM credentials
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public List getSnmpUsmCredentials() throws IOException, NXCException
   {
      final NXCPMessage msg = newMessage(NXCPCodes.CMD_GET_USM_CREDENTIALS);
      sendMessage(msg);
      final NXCPMessage response = waitForRCC(msg.getMessageId());

      int count = response.getFieldAsInt32(NXCPCodes.VID_NUM_RECORDS);
      ArrayList list = new ArrayList(count);
      long varId = NXCPCodes.VID_USM_CRED_LIST_BASE;
      for(int i = 0; i < count; i++, varId += 10)
      {
         list.add(new SnmpUsmCredential(response, varId));
      }

      return list;
   }

   /**
    * Update list of well-known SNMP USM credentials on server. Existing list
    * will be replaced by given one.
    *
    * @param list New list of SNMP USM credentials
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public void updateSnmpUsmCredentials(final List list) throws IOException, NXCException
   {
      final NXCPMessage msg = newMessage(NXCPCodes.CMD_UPDATE_USM_CREDENTIALS);

      msg.setFieldInt32(NXCPCodes.VID_NUM_RECORDS, list.size());
      long varId = NXCPCodes.VID_USM_CRED_LIST_BASE;
      for(int i = 0; i < list.size(); i++, varId += 10)
      {
         list.get(i).fillMessage(msg, varId);
      }

      sendMessage(msg);
      waitForRCC(msg.getMessageId());
   }

   /**
    * Get agent's master configuration file.
    *
    * @param nodeId Node ID
    * @return Master configuration file of agent running on given node
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public String getAgentConfig(long nodeId) throws IOException, NXCException
   {
      final NXCPMessage msg = newMessage(NXCPCodes.CMD_GET_AGENT_CONFIG);
      msg.setFieldInt32(NXCPCodes.VID_OBJECT_ID, (int) nodeId);
      sendMessage(msg);
      final NXCPMessage response = waitForRCC(msg.getMessageId());
      return response.getFieldAsString(NXCPCodes.VID_CONFIG_FILE);
   }

   /**
    * Update agent's master configuration file.
    *
    * @param nodeId Node ID
    * @param config New configuration file content
    * @param apply  Apply flag - if set to true, agent will restart automatically to
    *               apply changes
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public void updateAgentConfig(long nodeId, String config, boolean apply) throws IOException, NXCException
   {
      final NXCPMessage msg = newMessage(NXCPCodes.CMD_UPDATE_AGENT_CONFIG);
      msg.setFieldInt32(NXCPCodes.VID_OBJECT_ID, (int) nodeId);
      msg.setField(NXCPCodes.VID_CONFIG_FILE, config);
      msg.setFieldInt16(NXCPCodes.VID_APPLY_FLAG, apply ? 1 : 0);
      sendMessage(msg);
      waitForRCC(msg.getMessageId());
   }

   /**
    * Get list of parameters supported by entity defined by origin on given node.
    *
    * @param nodeId Node ID
    * @param origin data origin (agent, driver, etc.)
    * @return List of parameters supported by agent
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public List getSupportedParameters(long nodeId, int origin) throws IOException, NXCException
   {
      final NXCPMessage msg = newMessage(NXCPCodes.CMD_GET_PARAMETER_LIST);
      msg.setFieldInt32(NXCPCodes.VID_OBJECT_ID, (int) nodeId);
      msg.setFieldInt16(NXCPCodes.VID_FLAGS, 0x01); // Indicates request for parameters list
      msg.setFieldInt16(NXCPCodes.VID_DCI_SOURCE_TYPE, origin);
      sendMessage(msg);
      final NXCPMessage response = waitForRCC(msg.getMessageId());

      int count = response.getFieldAsInt32(NXCPCodes.VID_NUM_PARAMETERS);
      List list = new ArrayList(count);
      long baseId = NXCPCodes.VID_PARAM_LIST_BASE;
      for(int i = 0; i < count; i++)
      {
         list.add(new AgentParameter(response, baseId));
         baseId += 3;
      }
      return list;
   }
   
   /**
    * Get list of parameters supported by agent running on given node.
    *
    * @param nodeId Node ID
    * @return List of parameters supported by agent
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public List getSupportedParameters(long nodeId) throws IOException, NXCException
   {
      return getSupportedParameters(nodeId, DataCollectionObject.AGENT);
   }

   /**
    * Get list of tables supported by agent running on given node.
    *
    * @param nodeId Node ID
    * @return List of tables supported by agent
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public List getSupportedTables(long nodeId) throws IOException, NXCException
   {
      final NXCPMessage msg = newMessage(NXCPCodes.CMD_GET_PARAMETER_LIST);
      msg.setFieldInt32(NXCPCodes.VID_OBJECT_ID, (int) nodeId);
      msg.setFieldInt16(NXCPCodes.VID_FLAGS, 0x02); // Indicates request for table list
      msg.setFieldInt16(NXCPCodes.VID_DCI_SOURCE_TYPE, DataCollectionObject.AGENT);
      sendMessage(msg);
      final NXCPMessage response = waitForRCC(msg.getMessageId());

      int count = response.getFieldAsInt32(NXCPCodes.VID_NUM_TABLES);
      List list = new ArrayList(count);
      long baseId = NXCPCodes.VID_TABLE_LIST_BASE;
      for(int i = 0; i < count; i++)
      {
         list.add(new AgentTable(response, baseId));
         baseId += response.getFieldAsInt64(baseId);
      }
      return list;
   }

   /**
    * Get all events used in data collection by given node, cluster, or template object.
    *
    * @param objectId node, cluster, or template object ID
    * @return list of used event codes
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public long[] getDataCollectionEvents(long objectId) throws IOException, NXCException
   {
      final NXCPMessage msg = newMessage(NXCPCodes.CMD_GET_DCI_EVENTS_LIST);
      msg.setFieldInt32(NXCPCodes.VID_OBJECT_ID, (int)objectId);
      sendMessage(msg);
      final NXCPMessage response = waitForRCC(msg.getMessageId());
      if (response.getFieldAsInt32(NXCPCodes.VID_NUM_EVENTS) == 0) 
         return new long[0];
      return response.getFieldAsUInt32Array(NXCPCodes.VID_EVENT_LIST);
   }

   /**
    * Get names of all scripts used in data collection by given node, cluster, or template object.
    *
    * @param objectId node, cluster, or template object ID
    * @return list of used library scripts
    * @throws IOException  if socket I/O error occurs
    * @throws NXCException if NetXMS server returns an error or operation was timed out
    */
   public List