content: /* The contents of this file are subject to the terms
   * of the Common Development and Distribution License
   * (the License). You may not use this file except in
   * compliance with the License.
   *
   * You can obtain a copy of the License at
   * http://www.sun.com/cddl/cddl.html or
   * install_dir/legal/LICENSE
   * See the License for the specific language governing
   * permission and limitations under the License.
   *
   * When distributing Covered Code, include this CDDL
   * Header Notice in each file and include the License file
   * at install_dir/legal/LICENSE.
   * If applicable, add the following below the CDDL Header,
   * with the fields enclosed by brackets [] replaced by
   * your own identifying information:
   * "Portions Copyrighted [year] [name of copyright owner]"
   *
   * $Id$
   *
   * Copyright 2005-2009 Sun Microsystems Inc. All Rights Reserved
   */
  package com.sun.faban.harness.util;
  
  import com.sun.faban.harness.common.Config;
  import com.sun.faban.common.Command;
  import com.sun.faban.common.CommandHandle;
  import org.xml.sax.Attributes;
  import org.xml.sax.SAXException;
  import org.xml.sax.helpers.DefaultHandler;
  
  import javax.xml.parsers.SAXParser;
  import javax.xml.parsers.SAXParserFactory;
  import java.io.File;
  import java.io.FileInputStream;
  import java.io.IOException;
  import java.util.*;
  import java.util.logging.Level;
  import java.util.logging.Logger;
  
  /**
   * Checks the binary paths and the command mapping file and creates the exec
   * map.
   *
   * @author Akara Sucharitakul
   */
  public class CmdMap {
  
      static Logger logger = Logger.getLogger(CmdMap.class.getName());
  
      /**
       * Scans the bin directories and command map file and returns the command
       * map.
       * @param benchName The name of the benchmark,
       *                  null if the map is not benchmark-specific
       * @return The command map
       * @throws Exception Something went wrong obtaining the command map.
       */
      public static HashMap<String, List<String>> getCmdMap(String benchName)
              throws Exception {
          HashMap<String, List<String>> binMap =
                                          new HashMap<String, List<String>>();
  
          // The platform-specific and benchmark-specific binaries
          // take precedence, add last to map.
          File binDir = new File(Config.FABAN_HOME + "bin"); // $FABAN_HOME/bin
          addExecMap(binDir, binMap, null);
  
          logger.finer("Scanning OS-specific binaries in " + Config.OS_DIR);
          File sbinDir = new File(binDir, Config.OS_DIR); // $FABAN_HOME/bin/SunOS
          addExecMap(sbinDir, binMap, null);
  
          logger.finer("Scanning architecture-specific binaries in " +
                       Config.ARCH_DIR);
          sbinDir = new File(binDir, Config.ARCH_DIR); // $FABAN_HOME/bin/SunOS/sparc
          addExecMap(sbinDir, binMap, null);
  
          if (benchName != null)
              addResourceMap(binMap, Config.BENCHMARK_DIR, benchName);
  
          mapPathExt(binMap);
  
          addCmdMapFile(binMap);
  
          // Dump the binMap for debugging
          if (logger.isLoggable(Level.FINER)) {
              StringBuilder b = new StringBuilder("Executable map:\n");
              for (Map.Entry<String, List<String>> entry : binMap.entrySet()) {
                  b.append(entry.getKey());
                  b.append(" :");
                  List<String> l = entry.getValue();
                  for (String v : l) {
                      b.append(' ').append(v);
                  }
                  b.append('\n');
              }
              logger.finer(b.toString());
          }
         return binMap;
      }
  
      /**
       * Obtains the command map for a service/tool deployment.
       * @param serviceName The name of the service
       * @return The command map, if applicable, for that service
       * @throws Exception Something went wrong obtaining the command map.
       */
      public static HashMap<String, List<String>> getServiceBinMap(
                                      String serviceName) throws Exception {
          HashMap<String, List<String>> binMap =
                                          new HashMap<String, List<String>>();
  
          if (serviceName != null)
              addResourceMap(binMap, Config.SERVICE_DIR, serviceName);
  
          mapPathExt(binMap);
  
          addCmdMapFile(binMap);
  
          // Dump the binMap for debugging
          if (logger.isLoggable(Level.FINER)) {
              StringBuilder b = new StringBuilder("Executable map for service ");
              b.append(serviceName).append(":\n");
              for (Map.Entry<String, List<String>> entry : binMap.entrySet()) {
                  b.append(entry.getKey());
                  b.append(" :");
                  List<String> l = entry.getValue();
                  for (String v : l) {
                      b.append(' ').append(v);
                  }
                  b.append('\n');
              }
              logger.finer(b.toString());
          }
         return binMap;
  
      }
  
  
      private static void addResourceMap(HashMap<String, List<String>> binMap,
                                         String baseDir, String resourceName) {
          // chmod is the way to make a file executable on Unix. Other platforms
          // like Win32 does not have it and uses a different mechanism. So
          // we'll run chmod only if it's there.
          File chmodCmd = new File("/bin/chmod");
          if (!chmodCmd.exists()) {
              chmodCmd = new File("/usr/bin/chmod");
              if (!chmodCmd.exists())
                  chmodCmd = null;
          }
  
          ArrayList<String> chmod = null;
          if (chmodCmd != null) {
              chmod = new ArrayList<String>();
              chmod.add(chmodCmd.getAbsolutePath());
              chmod.add("+x");
          }
          File binDir = new File(baseDir + resourceName + "/bin/");
          boolean emptyList = addExecMap(binDir, binMap, chmod);
          File sbinDir = new File(binDir, Config.OS_DIR);
          emptyList = addExecMap(sbinDir, binMap, chmod) && emptyList;
          sbinDir = new File(binDir, Config.ARCH_DIR);
          emptyList = addExecMap(sbinDir, binMap, chmod) && emptyList;
          if (!emptyList)
              try {
                  logger.fine("Changing mode for bin directories.");
                  Command cmd = new Command(chmod);
                  CommandHandle handle = cmd.execute();
                  int exitValue = handle.exitValue();
                  if (exitValue != 0)
                      logger.severe("Failed to chmod bin files. Exit value is " +
                                                                      exitValue);
              } catch (IOException e) {
                  logger.log(Level.SEVERE, "Cannot change mode on bin files", e);
              } catch (InterruptedException e) {
                  logger.log(Level.SEVERE,
                             "Interrupted changing mode on bin files", e);
              }
      }
  
      private static boolean addExecMap(File binDir,
                                        HashMap<String, List<String>> binMap,
                                        List<String> chmod) {
          boolean emptyList = true;
          if (binDir.isDirectory()) {
              File[] binFiles = binDir.listFiles();
              for (int i = 0; i < binFiles.length; i++)
                  if (!binFiles[i].isDirectory()) {
                      String name = binFiles[i].getName();
                      String fullPath = binFiles[i].getAbsolutePath();
                      ArrayList<String> v = new ArrayList<String>();
                      v.add(fullPath);
                      binMap.put(name, v);
                      if (chmod != null) {
                          chmod.add(fullPath);
                          emptyList = false;
                      }
                  }
          }
          return emptyList;
      }
  
      /**
       * Obtains a list of path extensions valid in this environment.
       * @return A string array of valid path extensions
       */
      public static String[] getPathExt() {
          String pathExt = System.getProperty("faban.pathext");
  
          // Check the environment on a Windows system, in case
          // the faban.pathext property is not set.
          if (pathExt == null && File.separatorChar == '\\')
              pathExt = System.getenv("PATHEXT");
  
          if (pathExt == null)
              return null;
          pathExt = pathExt.trim();
          if (pathExt.length() == 0)
              return null;
  
          // Ensure each of the exts are lowercase.
          String[] pathExts = pathExt.split(File.pathSeparator);
          for (int i = 0; i < pathExts.length; i++) {
              pathExts[i] = pathExts[i].toLowerCase();
          }
          return pathExts;
      }
  
      /**
       * The mapPathExt modifies the binMap according to the Win32
       * conventions. For example, faban.cmd can be called with just faban.
       * But calling it with faban.cmd also works. The PATHEXT needs to be
       * passed to the JVM as a system property faban.pathext. PATHEXT
       * matching is case insensitive. On Win32-like systems, faban.pathext
       * would be set at the JVM invocation. On Unix systems, this property
       * should not be set. This method would be a noop in this case.
       * @param binMap The binMap
       */
      private static void mapPathExt(Map<String, List<String>> binMap) {
  
          String[] pathExts = getPathExt();
          if (pathExts == null)
              return;
  
          // Use a separate map so we don't modify binmap while iterating
          HashMap<String, List<String>> pathExtMap =
                                  new HashMap<String, List<String>>();
  
          Set<String> binKeys = binMap.keySet();
  
          // Scan from back to front so the frontmost one put latest
          // overrides the others that were put before.
          for (int i = pathExts.length - 1; i >= 0; i--)
              for (String key : binKeys)
                  if (key.toLowerCase().endsWith(pathExts[i])) {
                      List<String> value = binMap.get(key);
                      String newKey = key.substring(0, key.length() -
                                      pathExts[i].length());
                      pathExtMap.put(newKey, value);
                  }
  
          binMap.putAll(pathExtMap);
      }
  
  
      /**
       * Reads the command map file and adds/modifies the exec map accordingly.
       * @param binMap
       * @throws Exception
       */
      private static void addCmdMapFile(Map<String, List<String>> binMap)
              throws Exception {
          ArrayList<CmdDetail> cmdList = new ArrayList<CmdDetail>();
  
          File cmdMap = new File(
                              Config.CONFIG_DIR + Config.OS_DIR + "cmdmap.xml");
  
          if (cmdMap.exists()) {
  
              FileInputStream is = new FileInputStream(cmdMap);
              parseStream(is, cmdList);
  
              // Next step is to use the map command path and prefixes to the
              // actual executable.
              for (Iterator<CmdDetail> iter = cmdList.iterator();
                   iter.hasNext();) {
                  CmdDetail c = iter.next();
                  if (c.exec == null) {
                      c.exec = binMap.get(c.name);
                  } else {
                      String cmd = c.exec.get(0);
                      File f = new File(cmd); // The exec can still be in
                      if (!f.isAbsolute()) {     // the Faban path. May need
                          List<String> path = binMap.get(cmd); // another mapping.
                          if (path != null)      // if not absolute path. Just
                              c.exec.set(0, path.get(0)); // ignore if not found in map.
                      }                          // Should be in OS path instead.                        
                  }
                  if (c.exec == null) {
                      c.exec = new ArrayList<String>();
                      c.exec.add(c.name);
                  }
                  for (List<String> prefix : c.prefix) {
                      String cmd = prefix.get(0);
                      List<String> path = binMap.get(cmd);
                      if (path != null)
                          replaceFirst(prefix, path);
                  }
              }
  
              for (CmdDetail cmd : cmdList) {
                  ArrayList<String> exec = new ArrayList<String>();
                  for (List<String> prefix : cmd.prefix)
                      exec.addAll(prefix);
                  exec.addAll(cmd.exec);
                  binMap.put(cmd.name, exec);
              }
          }
      }
  
      private static void parseStream(FileInputStream is,
                                      ArrayList<CmdDetail> cmdList)
              throws Exception {
          SAXParserFactory sFact = SAXParserFactory.newInstance();
          sFact.setFeature("http://xml.org/sax/features/validation", false);
          sFact.setFeature("http://apache.org/xml/features/" +
                  "allow-java-encodings", true);
          sFact.setFeature("http://apache.org/xml/features/nonvalidating/" +
                  "load-dtd-grammar", false);
          sFact.setFeature("http://apache.org/xml/features/nonvalidating/" +
                  "load-external-dtd", false);
          SAXParser parser = sFact.newSAXParser();
          parser.parse(is, new MapReaderHandler(cmdList));
      }
  
      static class CmdDetail {
          String name;
          List<String> exec;
          List<List<String>> prefix;
  
          public String toString() {
              StringBuilder b = new StringBuilder();
              b.append(name);
              b.append(':');
  
              for (List<String> p : prefix)
                  for (String e : p)
                      b.append(e).append(' ');
  
              b.append(exec);
              return b.toString();
          }
      }
  
      static class MapReaderHandler extends DefaultHandler {
          private CmdDetail currentCmd;
          private ArrayList stack = new ArrayList();
          private StringBuilder buffer = new StringBuilder();
          private TreeMap<Integer, String> prefixes =
                  new TreeMap<Integer, String>();
          private int currentSequence = Integer.MIN_VALUE;
          private ArrayList<CmdDetail> cmdList;
  
          MapReaderHandler(ArrayList<CmdDetail> cmdList) {
              this.cmdList = cmdList;
          }
  
          /**
           * Receive notification of the start of an element.
           * <p/>
           * <p>By default, do nothing.  Application writers may override this
           * method in a subclass to take specific actions at the start of
           * each element (such as allocating a new tree node or writing
           * output to a file).</p>
           *
           * @param uri        The Namespace URI, or the empty string if the
           *                   element has no Namespace URI or if Namespace
           *                   processing is not being performed.
           * @param localName  The local name (without prefix), or the
           *                   empty string if Namespace processing is not being
           *                   performed.
           * @param qName      The qualified name (with prefix), or the
           *                   empty string if qualified names are not available.
           * @param attributes The attributes attached to the element.  If
           *                   there are no attributes, it shall be an empty
           *                   Attributes object.
           * @throws org.xml.sax.SAXException Any SAX exception, possibly
           *                                  wrapping another exception.
           * @see org.xml.sax.ContentHandler#startElement
           */
          public void startElement(String uri, String localName, String qName,
                                   Attributes attributes) throws SAXException {
              stack.add(qName);
              if ("command".equals(qName)) {
                  currentCmd = new CmdDetail();
              } else if ("prefix".equals(qName)) {
                  String s = attributes.getValue("sequence");
                  if (s != null) {
                      currentSequence = Integer.parseInt(s);
                      if (currentSequence < 0)
                          throw new SAXException(
                                  "Prefix sequence must be 0 or greater");
                  }
              }
          }
  
          /**
           * Receive notification of the end of an element.
           * <p/>
           * <p>By default, do nothing.  Application writers may override this
           * method in a subclass to take specific actions at the end of
           * each element (such as finalising a tree node or writing
           * output to a file).</p>
           *
           * @param uri       The Namespace URI, or the empty string if the
           *                  element has no Namespace URI or if Namespace
           *                  processing is not being performed.
           * @param localName The local name (without prefix), or the
           *                  empty string if Namespace processing is not being
           *                  performed.
           * @param qName     The qualified name (with prefix), or the
           *                  empty string if qualified names are not available.
           * @throws org.xml.sax.SAXException Any SAX exception, possibly
           *                                  wrapping another exception.
           * @see org.xml.sax.ContentHandler#endElement
           */
          public void endElement(String uri, String localName, String qName)
                  throws SAXException {
  
              int depth = stack.size();
  
              if (!stack.remove(depth - 1).equals(qName))
                  throw new SAXException("endElement mismatch: " + qName);
  
              if ("command".equals(qName)) {
                  if (currentCmd.name == null)
                      throw new SAXException("Command name not specified");
                  // Get all the prefixes
                  int prefixSize = prefixes.size();
                  currentCmd.prefix = new ArrayList<List<String>>();
                  if (prefixSize > 0) {
                      for (Map.Entry<Integer, String> entry : prefixes.entrySet())
                          currentCmd.prefix.add(Command.parseArgs(
                                                              entry.getValue()));
                  }
                  prefixes.clear();
                  cmdList.add(currentCmd);
  
              } else if ("name".equals(qName)) {
                  currentCmd.name = buffer.toString().trim();
              } else if ("exec".equals(qName)) {
                  currentCmd.exec = Command.parseArgs(buffer.toString().trim());
              } else if ("prefix".equals(qName)) {
                  if (currentSequence < 0)
                      currentSequence = -1;
                  String oldPrefix =
                          prefixes.put(currentSequence, buffer.toString().trim());
                  if (oldPrefix != null) {
                      if (currentSequence == -1)
                          throw new SAXException("Need to specify prefix " +
                                  "sequence for more than one prefix");
                      else
                          throw new SAXException("Duplicate prefix sequence " +
                                  "found");
                  }
                  currentSequence = Integer.MIN_VALUE;
              }
              buffer.setLength(0);
          }
  
          /**
           * Receive notification of character data inside an element.
           * <p/>
           * <p>By default, do nothing.  Application writers may override this
           * method to take specific actions for each chunk of character data
           * (such as adding the data to a node or buffer, or printing it to
           * a file).</p>
           *
           * @param ch     The characters.
           * @param start  The start position in the character array.
           * @param length The number of characters to use from the
           *               character array.
           * @see org.xml.sax.ContentHandler#characters
           */
          public void characters(char ch[], int start, int length) {
              buffer.append(ch, start, length);
          }
      }
  
      /**
       * Replace the first element of the command list with the relacement.
       * The original list will likely grow.
       * @param orig The original command list
       * @param replacement The replacement for the first entry in the list
       */
      public static void replaceFirst(List orig, List replacement) {
          ArrayList tmp = new ArrayList();
          tmp.addAll(orig);
          orig.clear();
          orig.addAll(replacement);
          for (int i = 1; i < tmp.size(); i++) {
              orig.add(tmp.get(i));
          }
      }
  
      /**
       * Main to test the command map.
       * @param args The command line argument
       * @throws Exception If the mapping shows any errors
       */
      public static void main(String[] args) throws Exception {
          ArrayList<CmdDetail> cmdList = new ArrayList<CmdDetail>();
          HashMap<String, List<String>> binMap =
                  new HashMap<String, List<String>>();
          FileInputStream is = new FileInputStream("cmdmap.xml");
          parseStream(is, cmdList);
  
          // Next step is to use the map command path and prefixes to the
          // actual executable.
          for (CmdDetail c : cmdList) {
              if (c.exec == null)
                  c.exec = binMap.get(c.name);
              if (c.exec == null) {
                  c.exec = new ArrayList<String>();
                  c.exec.add(c.name);
              }
  
              // Try to map exec cmd back to the binMap
              String exec = c.exec.get(0);
  
              // Search cmd for path separators ('/')
              int pathSepIdx = exec.indexOf(File.separator);
  
              // If not found, still a relative path
              if (pathSepIdx < 0) {
                  // Map it
                  List<String> execList = binMap.get(exec);
                  // And combine back with args
                  if (execList != null)
                      replaceFirst(c.exec, execList);
              }
  
              for (List<String> prefix : c.prefix) {
                  String cmd = prefix.get(0);
                  List<String> execList = binMap.get(cmd);
                  if (execList != null) {
                      replaceFirst(prefix, execList);
                  }
              }
          }
  
          for (Iterator<CmdDetail> iter = cmdList.iterator();
               iter.hasNext();) {
               CmdDetail c = iter.next();
               System.out.println(c.toString());
          }
      }
  }
  