/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.apache.vinci.transport.vns.service;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.util.ArrayList;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.StringTokenizer;

import org.apache.vinci.debug.Debug;
import org.apache.vinci.transport.BaseClient;
import org.apache.vinci.transport.BaseServer;
import org.apache.vinci.transport.BaseServerRunnable;
import org.apache.vinci.transport.ErrorFrame;
import org.apache.vinci.transport.Frame;
import org.apache.vinci.transport.KeyValuePair;
import org.apache.vinci.transport.ServiceException;
import org.apache.vinci.transport.Transportable;
import org.apache.vinci.transport.VinciFrame;
import org.apache.vinci.transport.VinciServableAdapter;
import org.apache.vinci.transport.vns.VNSConstants;

/**
 * VNS (Vinci Naming Service) provides the "directory" of all available Vinci services. VNS must be
 * running somewhere on the network for VinciClient and VinciServer classes to function. These
 * classes consult org.apache.vinci.transport.vns.client.VNSConfig to determine the location of the
 * VNS service.
 */
public class VNS extends VinciServableAdapter {

  private HashMap cachedResults = new HashMap();

  public static final String dirCmdAddService = "addservice";

  public static final String dirCmdAddAlias = "addalias";

  public static final String dirCmdDelService = "delservice";

  public static final String dirCmdDelAlias = "delalias";

  public static final String dirCmdUpdateService = "updateservice";

  public static final String dirCmdGetList = "getlist";

  public static final String dirCmdGetNames = "getnames";

  public static final String dirCmdGetHits = "gethits";

  public String ENV_PROXY = "vinci.environment.proxy";

  // Static variables set up during the start of the program
  private static String configFile, backupFile, workspacesFile, counterFile;

  private static String logFile = null;

  private static String configDir = null;

  private static int backupInterval = 120;

  private static int srvPort = 9000;

  private static boolean logFlag = false;

  private static String bindAddress = null;

  private static int backlog = 100;

  private static int maxThreads = 200;

  /* Instance variables */
  int port;

  String myLogFile = null;

  Thread backupThread;

  BackupThread backupThreadRunnable;

  Hashtable hits; // for keeping track of the counters

  int totalhits;

  ServiceRegistry SR;

  WorkspaceConfig WS;

  Writer log; // for writing to the logfile

  String starttime;

  static File quitFile;

  private static void setConfigDir(String path) {
    if (path.charAt(path.length() - 1) == File.separatorChar) {
      path = path.substring(0, path.length() - 1);
    }
    configDir = path;
    configFile = path + File.separatorChar + "vns.services";
    backupFile = path + File.separatorChar + "vns.services.bak";
    workspacesFile = path + File.separatorChar + "vns.workspaces";
    counterFile = path + File.separatorChar + "vns.counter";
    quitFile = new File(configDir + File.separatorChar + "quit");
    quitFile.delete();
  }

  public static void main(String[] args) throws IOException {
    Debug.setThreadNameOutput(true);
    System.setProperty("sun.net.inetaddr.ttl", "30");
    // ^^ JDK1.3
    java.security.Security.setProperty("networkaddress.cache.ttl", "30");
    // ^^ JDK1.4
    // ^^ REQUIRED to keep Java from caching "null" IP addresses for domain names
    setConfigDir(".");
    // Parse the commandline options
    String s;
    for (int i = 0; i < args.length; i++) {
      s = args[i].trim();
      if (s.equals("-h") || s.equals("--help")) {
        printUsage();
        return;
      }
      if (s.equals("-l") || s.equals("--logfile")) {
        logFile = args[++i];
        logFlag = true;
        Debug.p("Log file set to: " + logFile);
        continue;
      }
      if (s.equals("-p") || s.equals("--port")) {
        srvPort = Integer.parseInt(args[++i]);
        Debug.p("Service port set to: " + srvPort);
        continue;
      }
      if (s.equals("-b") || s.equals("--bind")) {
        bindAddress = args[++i];
        Debug.p("Bind address set to: " + bindAddress);
        continue;
      }
      if (s.equals("-g") || s.equals("--backlog")) {
        backlog = Integer.parseInt(args[++i]);
        Debug.p("Backlog set to: " + backlog);
        continue;
      }
      if (s.equals("-m") || s.equals("--maxthreads")) {
        maxThreads = Integer.parseInt(args[++i]);
        Debug.p("Max threads set to: " + maxThreads);
        continue;
      }
      if (s.equals("-i") || s.equals("--interval")) {
        backupInterval = Integer.parseInt(args[++i]);
        Debug.p("Backup interval set to: " + backupInterval);
        continue;
      }
      if (s.equals("-a") || s.equals("--logging")) {
        Debug.p("Access logging enabled.");
        logFlag = true;
        continue;
      }
      if (s.equals("-c") || s.equals("--config")) {
        String dir = args[++i];
        Debug.p("Config directory set to: " + dir);
        setConfigDir(dir);
        continue;
      }

      System.out.println("Unrecognized option : " + args[i]);
      printUsage();
      return;
    }

    // Start the server
    startServing();
  }

  private static void printUsage() {
    System.out.println("Usage: java org.apache.vinci.transport.vns.service.VNS [options]");
    System.out.println("  -p N  --port N");
    System.out.println("    Serve on port number N [default=" + srvPort + "]");

    System.out.println("  -b ip_address  --bind ip_address");
    System.out
            .println("    Force socket server to bind only to ip_address [default is bind to all]");

    System.out.println("  -c dirname --config dirname");
    System.out.println("    Look for & write config files in this directory [default=.]");

    System.out.println("  -i N  --interval N");
    System.out.println("    Backup every N seconds [default=" + backupInterval + "]");

    System.out.println("  -l fname  --logfile fname");
    System.out.println("    Access log pathname [default=<configdir>/vns.log]");

    System.out.println("  -a  --logging");
    System.out.println("    Enable access logging (implied by -l/--logfile option)");

    System.out.println("  -g N  --backlog N");
    System.out.println("    Set the ServerSocket backlog [default=" + backlog + "]");

    System.out.println("  -m N  --maxthreads N");
    System.out.println(
            "    Set the maximum size of the VNS thread pool [default=" + maxThreads + "].");

    System.out.println("  -h  --help");
    System.out.println("    This help message");

    System.exit(0);
  }

  /* Main run method to initialize the server and start running it */
  public static void startServing() throws IOException {
    VNS vns;
    if (logFlag) {
      if (logFile == null) {
        logFile = configDir + File.separatorChar + "vns.log";
      }
      vns = new VNS(srvPort, logFile);
    } else {
      vns = new VNS(srvPort);
    }

    // Load the settings files
    if (!vns.loadConfig(configFile)) {
      System.err.println("failed to load config file: " + configFile);
      System.err.println("Check its path and validity. If necessary, restore from backup.");
      System.exit(1);
    }

    vns.loadWorkspaces(workspacesFile);
    Debug.p("VNS Workspace : " + vns.WS.workspace);

    vns.loadCounters(counterFile);

    // Configure and start the backup thread
    Debug.p("Starting backup thread, using files " + backupFile + " and " + configFile);
    vns.backupThreadRunnable = new BackupThread(vns, backupFile, configFile, backupInterval,
            counterFile);

    vns.backupThread = new Thread(vns.backupThreadRunnable);
    vns.backupThread.start();

    // Start accepting connections and processing stuff
    Debug.p("Serving on port : " + vns.port);
    BaseServer server = null;
    if (bindAddress != null) {
      server = new BaseServer(vns) {
        @Override
        protected ServerSocket createServerSocket(int port) throws IOException {
          return new ServerSocket(port, backlog, InetAddress.getByName(bindAddress));
        }
      };
    } else {
      server = new BaseServer(vns) {
        @Override
        protected ServerSocket createServerSocket(int port) throws IOException {
          return new ServerSocket(port, backlog);
        }
      };
    }
    server.setSocketTimeout(5000);
    exitThread exitT = new exitThread(server);
    exitT.start();
    try {
      server.setThreadPoolSize(5, maxThreads);
      server.serve(vns.port);
    } catch (Throwable e) {
      Debug.reportException(e);
    } finally {
      Debug.p("Exiting.");
      System.exit(0);
    }
  }

  /* Constructors */
  public VNS() {
    SR = new ServiceRegistry();
    WS = new WorkspaceConfig(this);
    hits = new Hashtable();
    totalhits = 0;
    starttime = (new Date()).toString();
  }

  public VNS(int port) {
    this();
    this.port = port;
  }

  public VNS(int port, String logFile) throws IOException {
    this();
    this.port = port;
    this.myLogFile = new String(logFile);
    this.log = new BufferedWriter(new FileWriter(myLogFile, true));
  }

  /* Methods to load data from various files */
  public boolean loadConfig(String cFile) {
    File f = new File(cFile);
    if (!f.exists()) {
      Debug.p("WARNING: Config file doesn't exist, creating a new empty config file!");
      try {
        FileWriter writer = new FileWriter(f);
        writer.write("<CONFIGURATION></CONFIGURATION>");
        writer.close();
      } catch (IOException e) {
        Debug.reportException(e);
        return false;
      }
    }

    Debug.p("Loading config file : " + cFile);

    synchronized (SR) {
      try {
        SR.load(cFile);
        return true;
      } catch (Exception e) {
        Debug.reportException(e);
      }
    }
    return false;
  }

  public void loadWorkspaces(String wFile) {
    Debug.p("Loading workspaces file : " + wFile);

    try (FileReader F = new FileReader(wFile)) {
      WS.load(F);
    } catch (Exception e) {
      Debug.reportException(e);
      Debug.p("WARNING: failed to load workspace.");
    }
  }

  public void loadCounters(String cFile) {
    Debug.p("Loading counter file : " + cFile);

    FileReader F;
    BufferedReader br;
    int line = 1;

    try {
      F = new FileReader(cFile);
    } catch (FileNotFoundException e) {
      Debug.p("Could not load the counter file : " + cFile);
      return;
    }
    br = new BufferedReader(F);

    try {
      String s = br.readLine();
      if (s == null || s.trim().equals("")) {
        Debug.p("Invalid counter file format at line 1: " + cFile);
        br.close();
        F.close();
        return;
      }

      StringTokenizer str = new StringTokenizer(s);
      if (!(str.countTokens() > 1)) {
        Debug.p("Invalid counter file format at line 1: " + cFile);
        br.close();
        F.close();
        return;
      }

      if (!str.nextToken().equals("TOTAL")) {
        throw new Exception("First line invalid - does not start with TOTAL");
      }

      totalhits = Integer.parseInt(str.nextToken());
      hits = new Hashtable();
      line++;

      while ((s = br.readLine()) != null) {
        str = new StringTokenizer(s);
        hits.put(str.nextToken(), new Integer(str.nextToken()));
        line++;
      }

      br.close();
      F.close();
    } catch (IOException e2) {
      Debug.p("IO Problem while reading from counter file : " + cFile);
      Debug.p("Exception was : " + e2);
    } catch (NumberFormatException e3) {
      Debug.p("Invalid number specified in counter file " + cFile + " at line " + line);
    } catch (Exception e4) {
      Debug.p("Problem while parsing counter file : " + cFile);
      Debug.p("Exception generated : " + e4);
    } finally {
      try {
        br.close();
      } catch (IOException e5) {
      }
      try {
        F.close();
      } catch (IOException e6) {
      }
    }
  }

  /* Method to save data to various files */
  public void saveConfig(String cFile) {
    Debug.p("Saving to config file : " + cFile);
    long startTime = System.currentTimeMillis();
    synchronized (SR) {
      Writer F = null;
      try {
        F = new BufferedWriter(new FileWriter(cFile + ".rename"));
        try {
          SR.save(F);
        } finally {
          F.close();
        }
        File real_file = new File(cFile);
        real_file.delete();
        if (!new File(cFile + ".rename").renameTo(real_file)) {
          throw new IOException("FAILED to rename services config file!!!");
        }
      } catch (IOException e) {
        Debug.reportException(e);
        Debug.p("Could not save config file : " + cFile);
      }
    }
    startTime = System.currentTimeMillis() - startTime;
    Debug.p("Config save required " + startTime + " millis.");
    // flush the log too if it needs it...
    if (log != null) {
      synchronized (log) {
        try {
          log.flush();
        } catch (IOException e) {
          Debug.reportException(e);
        }
      }
    }
  }

  public void saveCounters(String cFile) {
    Debug.p("Saving counter file : " + cFile);

    FileWriter F;

    try {
      F = new FileWriter(cFile);
    } catch (IOException e) {
      Debug.p("Could not save the counter file : " + cFile);
      Debug.p("Exception generated : " + e);
      return;
    }

    try {
      F.write("TOTAL " + totalhits + "\n");
      if (hits == null) {
        F.close();
        return;
      }
      Enumeration keys = hits.keys();
      Integer value;
      String key;
      while (keys.hasMoreElements()) {
        key = (String) keys.nextElement();
        value = (Integer) hits.get(key);
        F.write(key.trim() + " " + value + "\n");
      }
    } catch (IOException e2) {
      Debug.reportException(e2);
      Debug.p("IO Problem while writing to counter file : " + cFile);
    } finally {
      try {
        if (F != null) {
          F.close();
        }
      } catch (IOException e3) {
        Debug.reportException(e3);
      }
    }
  }

  public void saveWorkspaces(String wFile) {
    Debug.p("Loading workspaces file : " + wFile);

    Writer F = null;
    try {
      F = new BufferedWriter(new FileWriter(wFile));
      WS.save(F);
      F.close();
    } catch (IOException e) {
      Debug.p("Could not save workspaces file : " + wFile);
      Debug.p("Exception generated : " + e);
    } catch (RuntimeException e2) {
      Debug.p("Problem while saving workspace file : " + wFile);
      Debug.p("Exception generated: " + e2);
    } finally {
      try {
        if (F != null) {
          F.close();
        }
      } catch (IOException e) {
        Debug.reportException(e);
      }
    }

  }

  /* Main processing routine */
  @Override
  public Transportable eval(Transportable inp) throws ServiceException {
    VinciFrame in = (VinciFrame) inp;
    VinciFrame out = null;

    Debug.p("Request from " + in.fgetString("vinci:REMOTEIP"));

    String command;
    command = in.fgetString("vinci:COMMAND");
    if (command == null) {
      out = new VinciFrame();
      out.fadd("vinci:ERROR", "Malformed request");
      return out;
    }

    try {
      if (VNSConstants.RESOLVE_COMMAND.equals(command)) {
        out = resolve(in);
      } else if (VNSConstants.SERVEON_COMMAND.equals(command)) {
        out = serveon(in);
      } else if (command.equals(dirCmdAddService))
        out = addService(in);
      else if (command.equals(dirCmdAddAlias))
        out = addAlias(in);
      else if (command.equals(dirCmdUpdateService))
        out = updateService(in);
      else if (command.equals(dirCmdDelService))
        out = delService(in);
      else if (command.equals(dirCmdDelAlias))
        out = delAlias(in);
      else if (command.equals(dirCmdGetList))
        out = getList(in);
      else if (command.equals(dirCmdGetNames))
        out = getNames(in);
      else if (command.equals(dirCmdGetHits))
        out = getHits(in);
      else {
        out = new VinciFrame();
        out.fadd("vinci:ERROR", "Unrecognized command");
      }
    } catch (Exception e) {
      // Exact translation of the Python code [may not be necessary]
      out = new VinciFrame();
      out.fadd("vinci:ERROR", "Critical error :\n" + e
              + "\nThe server MAY not respond to further requests.\nPlease notify the administrator\n");
      e.printStackTrace();
    }

    return out;
  }

  /* Various processing routines */
  VinciFrame resolveLocal(VinciFrame in) {

    Debug.p("Local resolve");

    VinciFrame out = new VinciFrame();

    String name = in.fgetString("SERVICE");
    if (strip(name) == null)
      return new ErrorFrame("No service name specified");

    String host = strip(in.fgetString("HOST"));
    String realhost = strip(in.fgetString("IP"));
    if (strip(realhost) == null && strip(host) != null) {
      try {
        // Try to resolve the IP if possible
        realhost = (InetAddress.getByName(host)).getHostAddress();
      } catch (Exception e) {
        // realhost = null;

        // return error message
        return new ErrorFrame("Could not resolve IP address for service " + name);
      }
    }

    String instance = in.fgetString("INSTANCE");
    int inst = -1;
    try {
      inst = Integer.parseInt(instance);
    } catch (Exception e) {
      inst = -1;
    }

    String level = in.fgetString("LEVEL");

    // Default as specified in SPEC.txt
    if (strip(level) == null || level.trim().toLowerCase().equals("none"))
      level = "-1";
    else if (level.toLowerCase().equals("all")) {
      Debug.p("Specific level must be given (not all)");
      out.fadd("vinci:ERROR", "Specific level must be given (not all)");
      return out;
    }

    synchronized (SR) {
      // Construct a valid number from the level string specified
      level = "" + SR.getLevel(name, level);
    }
    Service[] servicesList;

    // Check the cache for matches
    CachedItem citem = (CachedItem) checkCache(name + "[" + level + "]");
    if (citem == null) {
      synchronized (SR) {
        // Find all services that match the specified name and are <= level specified
        // Also, resolve all aliases to actual services
        servicesList = SR.getServices(name, level, true);
      }
      // Cache the result (if any matches found)
      if (servicesList != null && servicesList.length > 0) {
        // Cache the resolve result under the actual name and level returned
        cache(name + "[" + servicesList[0].level + "]", new CachedItem(servicesList));
        // Have a proxy pointer to the entry created if it is not the same
        if (!servicesList[0].level.equals(level)) {
          cache(name + "[" + level + "]",
                  new ProxyCachedItem(name + "[" + servicesList[0].level + "]"));
        }
      }
    } else
      servicesList = citem.servicesList;

    if (servicesList == null) {
      Debug.p("Service " + name + " not found");
      out.fadd("vinci:ERROR", "Service " + name + " not found");
      return out;
    }

    Debug.p("Number of services found with name = " + name + ", and level = " + level + " : "
            + servicesList.length);

    if (servicesList.length == 0)
      System.err.println("NO SERVICES FOUND WITH REALHOST = " + realhost + " name = " + name);

    ArrayList v = new ArrayList();

    // Filter the services with matching realhost and instance
    for (int i = 0; i < servicesList.length; i++) {
      if (realhost != null && !servicesList[i].realhost.equals(realhost))
        continue;
      if (inst > -1 && inst != servicesList[i].instance)
        continue;
      v.add(servicesList[i]);
    }

    servicesList = new Service[v.size()];

    for (int i = 0; i < v.size(); i++)
      servicesList[i] = (Service) v.get(i);

    if (servicesList == null || servicesList.length == 0) {
      Debug.p("Service " + name + " not found");
      out.fadd("vinci:ERROR", "Service " + name + " not found");
      return out;
    }

    VinciFrame temp;
    for (int j = 0; j < servicesList.length; j++) {
      temp = new VinciFrame();

      temp.fadd("HOST", (String) servicesList[j].getAttr("host"));
      temp.fadd("PORT", "" + servicesList[j].port);
      temp.fadd("INSTANCE", "" + servicesList[j].instance);

      if (servicesList[j].meta) {
        Object o = servicesList[j].dict.get("META");
        if (o instanceof String)
          temp.fadd("META", (String) o);
        if (o instanceof Frame)
          temp.fadd("META", (Frame) o);
      }

      out.fadd("SERVER", temp);
    }

    out.fadd("LEVEL", servicesList[0].level); // should all be the same
    return out;
  }

  VinciFrame resolveProxy(VinciFrame in, String workspace) {
    Debug.p("Proxy resolve");
    if (workspace == null)
      workspace = in.fgetString("WORKSPACE");

    if (workspace == null || workspace.equals(WS.workspace)) {
      // New spec - assume it is local
      return resolveLocal(in);
    }

    Service[] services = SR.getServices(ENV_PROXY);
    VinciFrame out = new VinciFrame();
    if (services == null || services.length == 0) {
      out.fadd("vinci:ERROR", "Cannot locate proxy service");
      return out;
    }

    // Iterate through all the registered proxy services and try to resolve at each
    for (int i = 0; i < services.length; i++) {
      Service S = services[i];
      if (S.realhost == null) {
        continue; // Can't do anything if realhost isn't specified
      }

      try {
        // If the host specified is the local one, then use resolveLocal()
        // else it will have an infinite loop
        if (S.port == srvPort && InetAddress.getLocalHost().getHostAddress().equals(S.realhost))
          out = resolveLocal(in);
        else {
          Debug.p("Resolving with VNS: " + S.realhost + ":" + S.port);
          out = BaseClient.rpc(in, S.realhost, S.port);
        }
        System.out.println(out.toXML());
        if (out == null || out instanceof ErrorFrame || strip(out.fgetString("LEVEL")) == null
                || out.fgetString("vinci:ERROR") != null) {
          continue; // Check if resolve actually worked
        }
        return out;
      } catch (ServiceException se) {
        // Don't need to worry - caused due to an ErrorFrame as the server was unable to
        // resolve
        System.out.println(se);
        se.printStackTrace();
      } catch (Exception e) {
        e.printStackTrace();
        out.fadd("vinci:ERROR", "Proxy forwarding failed due to " + e);
        return out;
      }
    }
    return new ErrorFrame("Proxy forwarding failed");
  }

  VinciFrame resolveDefaults(VinciFrame in) {
    String name = in.fgetString("SERVICE");
    String workspace;

    VinciFrame out;

    if (WS.workspace == null || WS.search.size() == 0)
      return resolveLocal(in); // Use resolveLocal() if WS was not defined

    for (int i = 0; i < WS.search.size(); i++) {
      workspace = ((String) WS.search.get(i));

      if (workspace.equals(WS.workspace)) {
        Debug.p("Resolving on local workspace ...");

        out = resolveLocal(in);
      } else {
        Debug.p("Resolving on workspace " + workspace + " ...");
        out = resolveProxy(in, workspace);
      }

      if (out.fgetString("vinci:ERROR") == null)
        return out;
    }

    Debug.p("Resolution failed");
    return new ErrorFrame("Could not find service " + name + " in default workspaces");
  }

  VinciFrame resolve(VinciFrame in) {
    logRequest(VNSConstants.RESOLVE_COMMAND, in.fgetString("vinci:REMOTEIP"),
            in.fgetString("SERVICE"));

    if (in.fgetString("WORKSPACE") == null) {
      return resolveDefaults(in);
    } else {
      Debug.p("Resolving on workspace " + in.fgetString("WORKSPACE") + " ...");
      return resolveProxy(in, null);
    }
  }

  VinciFrame serveon(VinciFrame in) {
    logRequest(VNSConstants.SERVEON_COMMAND, in.fgetString("vinci:REMOTEIP"), null);

    Service S = null, srv = null;
    VinciFrame out = new VinciFrame();

    String name = in.fgetString("SERVICE");
    if (strip(name) == null)
      return new ErrorFrame("Invalid service name specified : " + name);
    String host = in.fgetString("HOST");

    if (strip(host) == null) {
      Debug.p("Getting host from socket peer info");
      host = in.fgetString("vinci:REMOTEHOST");
      if (host == null)
        host = in.fgetString("vinci:REMOTEIP");
      if (host == null)
        return new ErrorFrame("Host could not be parsed - specify HOST");
      Debug.p("Peer host is : " + host);
    }

    String level = in.fgetString("LEVEL");

    // Default as specified in SPEC.txt
    if (strip(level) == null || level.trim().toLowerCase().equals("none"))
      level = "0";

    if (level.equals("all")) {
      out.fadd("vinci:ERROR", "Specific level must be given or none at all");
      return out;
    }

    String instance = in.fgetString("INSTANCE");
    try {
      instance = "" + Integer.parseInt(instance);
    } catch (Exception e) {
      instance = null;
    }

    if (instance == null)
      instance = "0";

    Debug.p("Host = " + host);
    String realhost = in.fgetString("IP");
    try {
      if (realhost == null)
        realhost = InetAddress.getByName(host).getHostAddress();
    } catch (Exception e) {
      out.fadd("vinci:ERROR", "Could not resolve IP due to Exception " + e);
      return out;
    }

    Debug.p("search: realhost = " + realhost + " - instance = " + instance);

    synchronized (SR) {
      level = "" + SR.getLevel(name, level);
    }

    Debug.p("Level = " + level);

    // Update the cache
    updateCache(name + "[" + level + "]");

    Object[] services;
    synchronized (SR) {
      services = SR.getServices(name, level);
    }

    if (services != null) {
      for (int i = 0; i < services.length; i++) {
        S = (Service) services[i];
        Debug.p("current: realhost = " + S.realhost + " - instance = " + S.instance);
        if (S.realhost.equals(realhost) && instance.equals("" + S.instance)
                && S.level.equals(level)) {
          srv = S;
          break;
        }
      }
    }

    if (srv == null) {
      Debug.p("Creating now");
      Hashtable H = new Hashtable();
      H.put("NAME", name);
      H.put("HOST", host);
      H.put("LEVEL", level);
      H.put("INSTANCE", instance);
      H.put("IP", realhost);
      srv = new Service(H);
      boolean ok = false;
      synchronized (SR) {
        Debug.p("Adding service : " + H.get("NAME") + ", lvl=" + H.get("LEVEL") + ",instance="
                + H.get("INSTANCE") + ",ip=" + H.get("IP"));
        ok = SR.addService(srv);
      }
      if (!ok) {
        out.fadd("vinci:ERROR", "COuld not find or add service " + name);
        return out;
      }
    } else {
      if (srv.minport != srv.maxport) { // if it isn't running on a fixed port
        final String s_host = srv.host;
        final int s_port = srv.port;
        new Thread(new Runnable() {
          @Override
          public void run() {
            Debug.p("Trying to shutdown old service ...");
            VinciFrame shutdown = new VinciFrame();
            shutdown.fadd("vinci:SHUTDOWN",
                    "Identical service started on this host. Use the INSTANCE tag to run multiple instances of the same service on a single host.");
            try {
              VinciFrame f = BaseClient.rpc(shutdown, s_host, s_port, 10000);
              Debug.p("Shutdown response received: " + f.toXML());
            } catch (Exception e) {
              Debug.p("Old service already closed: " + e);
            }
          }
        }).start();
      }
    }

    srv.updatePort();
    out.fadd("HOST", srv.host);
    out.fadd("PORT", srv.port);
    out.fadd("LEVEL", srv.level);
    out.fadd("INSTANCE", srv.instance);
    out.fadd("IP", srv.realhost);
    return out;
  }

  VinciFrame addService(VinciFrame in) {
    logRequest(dirCmdAddService, in.fgetString("vinci:REMOTEIP"), null);

    VinciFrame service = in.fgetVinciFrame("SERVICE");

    Hashtable H = new Hashtable();

    int total = service.getKeyValuePairCount();
    KeyValuePair P;
    for (int i = 0; i < total; i++) {
      P = service.getKeyValuePair(i);
      if (P.isValueALeaf()) {
        H.put(P.getKey(), P.getValueAsString());
      } else {
        H.put(P.getKey(), P.getValue());
      }
    }

    String level = (String) H.get("LEVEL");
    if (strip(level) == null) {
      H.put("LEVEL", "0"); // default level of 0
    }

    boolean ok = false;

    synchronized (SR) {
      Service S;
      ok = SR.addService(S = new Service(H));
      updateCache(S);
    }

    return getFrame(ok, "Add Service request failed");
  }

  VinciFrame addAlias(VinciFrame in) {
    logRequest(dirCmdAddAlias, in.fgetString("vinci:REMOTEIP"), null);

    VinciFrame service = in.fgetVinciFrame("SERVICE");

    boolean ok = true;

    if (service.fgetString("NAME") == null || service.fgetString("TARGET") == null)
      getFrame(false, "Malformed request");
    else {
      synchronized (SR) {
        ok = SR.addAlias(
                new ServiceAlias(service.fgetString("NAME"), service.fgetString("TARGET")));
      }
    }

    return getFrame(ok, "Add alias request failed");
  }

  VinciFrame delService(VinciFrame in) {
    logRequest(dirCmdDelService, in.fgetString("vinci:REMOTEIP"), null);

    VinciFrame service = in.fgetVinciFrame("SERVICE");

    Hashtable H = new Hashtable();

    int total = service.getKeyValuePairCount();
    KeyValuePair P = null;
    for (int i = 0; i < total; i++) {
      P = service.getKeyValuePair(i);

      if (P.isValueALeaf()) {
        H.put(P.getKey(), P.getValueAsString());
      } else {
        H.put(P.getKey(), P.getValue());
      }
    }

    String level = (String) H.get("LEVEL");
    if (strip(level) == null)
      H.put("LEVEL", "0"); // default level of 0

    boolean ok = false;

    synchronized (SR) {
      Service S;
      ok = SR.delService(S = new Service(H));
      updateCache(S);
    }

    return getFrame(ok, "Delete Service request failed");
  }

  VinciFrame delAlias(VinciFrame in) {
    logRequest(dirCmdDelAlias, in.fgetString("vinci:REMOTEIP"), null);
    VinciFrame service = in.fgetVinciFrame("SERVICE");
    boolean ok = true;
    if (service.fgetString("NAME") == null || service.fgetString("TARGET") == null) {
      getFrame(false, "Malformed request");
    } else {
      synchronized (SR) {
        ok = SR.delAlias(service.fgetString("NAME"));
      }
    }
    return getFrame(ok, "Delete alias request failed");
  }

  VinciFrame updateService(VinciFrame in) {
    logRequest(dirCmdUpdateService, in.fgetString("vinci:REMOTEIP"), null);

    VinciFrame service = in.fgetVinciFrame("SERVICE");

    Hashtable H = new Hashtable();

    int total = service.getKeyValuePairCount();
    KeyValuePair P;
    for (int i = 0; i < total; i++) {
      P = service.getKeyValuePair(i);
      if (P.isValueALeaf()) {
        H.put(P.getKey(), P.getValueAsString());
      } else {
        H.put(P.getKey(), P.getValue());
      }
    }

    boolean ok = false;

    synchronized (SR) {
      Service S;
      ok = SR.updateService(S = new Service(H));
      updateCache(S);
    }

    return getFrame(ok, "Update Service request failed");
  }

  VinciFrame getList(VinciFrame in) {
    String level = in.fgetString("LEVEL");

    // Default as specified in SPEC.txt
    if (level == null || level.trim().toLowerCase().equals("none")) {
      level = "-1";
    }

    String prefix = in.fgetString("PREFIX");
    if (prefix == null) {
      prefix = "";
    }

    logRequest(dirCmdGetList, in.fgetString("vinci:REMOTEIP"), prefix);

    Object[] servicelist;
    synchronized (SR) {
      Debug.p("Getting list for level : " + level);
      servicelist = SR.listServices(prefix, level);
      if (servicelist == null)
        servicelist = new Object[0]; // For safety
      Debug.p("Matches for getList : " + servicelist.length);
    }

    VinciFrame F = new VinciFrame();
    for (int i = 0; i < servicelist.length; i++) {
      if (ServiceAlias.isAlias(servicelist))
        F.fadd("SERVICE", ((ServiceAlias) servicelist[i]).toFrame());
      else
        F.fadd("SERVICE", ((Service) servicelist[i]).toFrame());
    }

    return F;
  }

  VinciFrame getNames(VinciFrame in) {
    String level = in.fgetString("LEVEL");

    // Default as specified in SPEC.txt
    if (level == null || level.trim().toLowerCase().equals("none"))
      level = "-1";

    String prefix = in.fgetString("PREFIX");
    if (prefix == null)
      prefix = "";

    logRequest(dirCmdGetNames, in.fgetString("vinci:REMOTEIP"), prefix);

    String[] servicelist;
    synchronized (SR) {
      Debug.p("Getting names list for level : " + level);
      servicelist = SR.listNames(prefix, level);
      if (servicelist == null)
        servicelist = new String[0]; // For safety
      Debug.p("Matches for getNames : " + servicelist.length);
    }

    VinciFrame F = new VinciFrame();
    for (int i = 0; i < servicelist.length; i++) {
      F.fadd("SERVICE", servicelist[i]);
    }

    return F;
  }

  VinciFrame getHits(VinciFrame in) {
    String type = in.fgetString("TYPE");

    VinciFrame F = new VinciFrame();
    VinciFrame temp;
    String key;

    if (type != null) {
      if (type.equals("all")) {
        Enumeration keys = hits.keys();
        while (keys.hasMoreElements()) {
          key = (String) keys.nextElement();
          temp = new VinciFrame();
          temp.fadd("TYPE", key);
          temp.fadd("COUNT", hits.get(key).toString());
          F.fadd("HITS", temp);
        }
        F.fadd("TOTAL", "" + totalhits);
      } else {
        if (hits.get(type) != null)
          F.fadd("HITS", hits.get(type).toString());
        else {
          F.fadd("vinci:ERROR", "No such type: " + type);
          return F;
        }
      }
    } else {
      F.fadd("HITS", "" + totalhits);
    }

    F.fadd("STARTED", "" + starttime);

    return F;
  }

  /* Caching routines */
  private void cache(String s, Object o) {
    synchronized (cachedResults) {
      cachedResults.put(s, o);
    }
  }

  private void updateCache(String s) {
    synchronized (cachedResults) {
      cachedResults.remove(s);
    }
  }

  private Service updateCache(Service S) {
    String name = (String) S.getAttr("name");
    String level = "";
    synchronized (SR) {
      level = "" + SR.getLevel(name, (String) S.getAttr("level"));
    }

    updateCache(name + "[" + level + "]");
    return S;
  }

  private Object checkCache(String s) {
    synchronized (cachedResults) {
      Object o = null;
      do {
        o = cachedResults.get(s);
        if (o == null)
          return null;
        if (o instanceof ProxyCachedItem)
          s = ((ProxyCachedItem) o).altKey;
      } while (!(o instanceof CachedItem));
      return o;
    }
  }

  /* Helper method to return the correct VinciFrame */
  VinciFrame getFrame(boolean ok, String err) {
    VinciFrame F = null;
    if (ok) {
      F = new VinciFrame();
      F.fadd("STATUS", "OK");
    } else
      return new ErrorFrame(err);

    return F;
  }

  /* Helper methods */
  public static String strip(String s) {
    if (s == null || s.trim().equals(""))
      return null;
    return s;
  }

  public static String emptyString(String s) {
    if (strip(s) == null)
      return "";
    return s;
  }

  /* Logging routines */
  void logRequest(String type, String ip, String text) {
    if (ip == null) {
      ip = BaseServerRunnable.getSocket().getInetAddress().getHostAddress();
      if (ip == null) {
        ip = "UNKNOWN";
      }
    }
    if (text == null)
      text = "";

    // Update hit counter
    synchronized (hits) {
      Integer I = (Integer) hits.get(type);
      if (I == null)
        I = 0;
      I = I + 1;

      hits.put(type, I);
      totalhits++;
    }
    String ts = (new Date()).toString();
    String write_me = ip + " - [" + ts + "] " + type + " " + text + "\n";
    Debug.p(write_me);

    if (log == null)
      return;

    synchronized (log) {
      // Make an entry in the log file
      try {
        log.write(write_me);
      } catch (IOException e) {
        Debug.reportException(e);
      }
    }
  }

  @Override
  public void cleanExit() {
    Debug.p("Exiting now ...");
    quitFile.delete();
    try {
      backupThreadRunnable.stop = true;
      backupThread.interrupt();
    } catch (Exception e) {
      Debug.reportException(e);
    }

    backupThreadRunnable.forceWrite();

    if (log != null) {
      try {
        synchronized (log) {
          log.close();
        }
      } catch (IOException e) {
        Debug.reportException(e);
      }
    }
  }

}

/* Class for the backup thread */
class BackupThread implements Runnable {
  VNS parent;

  String backupFile;

  String configFile = null;

  String counterFile = null;

  int interval;

  protected volatile boolean stop = false;

  public BackupThread(VNS parent, String backupFile, String configFile, int interval,
          String counterFile) {
    // We create new strings so that we can start another VNS instance without disturbing this one
    this.parent = parent;
    this.backupFile = new String(backupFile);
    if (configFile != null)
      this.configFile = new String(configFile);
    if (counterFile != null)
      this.counterFile = new String(counterFile);
    this.interval = interval * 1000; // Convert secs to millisecs
  }

  public void forceWrite() {
    parent.saveConfig(backupFile);
    if (configFile != null)
      parent.saveConfig(configFile);
    if (counterFile != null)
      parent.saveCounters(counterFile);
    Debug.p("All files written to disk");
  }

  @Override
  public void run() {
    Debug.p("Backup thread started");
    while (true) {
      parent.saveConfig(backupFile);
      if (configFile != null) {
        parent.saveConfig(configFile);
      }
      if (counterFile != null) {
        parent.saveCounters(counterFile);
      }
      try {
        Thread.sleep(interval);
      } catch (InterruptedException e) {
        break;
      }
      if (stop) {
        break;
      }
    }
    Debug.p("Backup thread exitted");
  }

}

/* Thread to check for quit command */
class exitThread extends Thread {
  BaseServer parent = null;

  boolean run = false;

  public exitThread(BaseServer parent) {
    this.parent = parent;
    run = true;
  }

  @Override
  public void run() {
    if (parent == null)
      return;

    int i = 0;

    char[] cmd = { 'q', 'u', 'i', 't' };
    System.out.println(">>>>>>>>>>>>> VNS is up and running! <<<<<<<<<<<<<<<<<");
    System.out.println(">>>>>>>>>>>>> Type \'quit\' and hit ENTER to terminate VNS <<<<<<<<<<<<<");
    while (run) {
      i = 0;

      try {
        if (System.in.available() > 0) {
          while (System.in.available() > 0) {
            if ((System.in.read()) != cmd[i])
              i = 0;
            else
              i++;
            if (i >= cmd.length) {
              run = false;
              break;
            }
          }
        }
      } catch (Exception e) {
      }

      try {
        Thread.sleep(1000);
        if (VNS.quitFile.exists()) {
          run = false;
        }
      } catch (InterruptedException e2) {
        run = false;
      }
    }

    System.out.println("Got quit command from user");
    parent.shutdownServing();
    System.out.println("Request handling thread successfully shutdown");
  }
}

/* Encapsulating class for cached item */
class CachedItem {

  CachedItem(Service[] s) {
    servicesList = s;
  }

  protected Service[] servicesList = null;
}

/* Points to some other cached entry */
class ProxyCachedItem {

  protected String altKey;

  ProxyCachedItem(String s) {
    altKey = s;
  }
}
