=== modified file 'src/com/goldencode/p2j/util/SourceNameMapper.java'
--- src/com/goldencode/p2j/util/SourceNameMapper.java	2018-02-26 16:06:37 +0000
+++ src/com/goldencode/p2j/util/SourceNameMapper.java	2018-05-07 16:24:49 +0000
@@ -83,6 +83,10 @@
 **                           - absolute name
 **                           - PROPATH
 **                           - direct match.
+** 039 CA  20180504          Added support for folder aliasing: these are specified via the
+**                           "path-aliases" per-server directory node, and provide a mapping of
+**                           alias folder name to their 'source' counterpart.  Also, added support
+**                           to identify the target source program for a RUN program.r statement.
 */
 
 /*
@@ -142,9 +146,12 @@
 
 import java.io.*;
 import java.util.*;
+import java.util.function.*;
+import java.util.logging.*;
 import java.net.*;
 import org.w3c.dom.*;
 import com.goldencode.util.*;
+import com.goldencode.p2j.directory.*;
 import com.goldencode.p2j.library.*;
 import com.goldencode.p2j.security.*;
 
@@ -184,6 +191,9 @@
  */
 public class SourceNameMapper
 {
+   /** logger */
+   private static final Logger LOG = LogHelper.getLogger(SourceNameMapper.class.getName());
+
    /** Context-local data. */
    private static final ContextLocal<WorkArea> local = new ContextLocal<WorkArea>()
    {
@@ -227,9 +237,17 @@
    /** Named events attribute. */
    private static final String PUBLISHED_EVENTS = "published-events";
    
+   /** Map of aliases (r-code folders) to their source folders (conversion folders). */
+   private static Map<String, String> pathAliases = null;
+   
    /** Progress procedure file name to Java class name map table. */
    private static volatile Map<String, ExternalProgram> p2jMap = null;
    
+   /**
+    * Map of program names without extension to the programs which can be resolved with this name. 
+    */
+   private static volatile Map<String, List<ExternalProgram>> p2jNonExtMap = null;
+   
    /** Progress Java class name to procedure file name map table. */
    private static Map<String, ExternalProgram> j2pMap = null;
    
@@ -851,7 +869,7 @@
       
       return extProg.search(iename, func);
    }
-   
+
    /**
     * Convert a Progress file name into Java class name. The input name can be an absolute file
     * name, a file name relative to current location or a file name relative to some directory
@@ -891,7 +909,15 @@
     * "c:/path/to/project/stcases/uast/rundir/absolute/program.p"
     * "rundir/../../uast/rundir/relative/test11.p."
     * "../uast/rundir/relative/test5.p"
-    *
+    * <p>
+    * If no program name is explicitly found with a <code>.r</code> extension, R-code program 
+    * names are looked up using the {@link #p2jNonExtMap} (the program name without the extension).
+    * <p> 
+    * If {@link #pathAliases} is specified, and the program can be found (as it is or after 
+    * removing the <code>.r</code> extension) in the propath, the lookup falls back to resolving
+    * the aliased folders in the program name, via {@link #resolvePathAliases}.
+    * 
+    * 
     * @param   legacyProgName
     *          Progress file name.
     *
@@ -901,6 +927,194 @@
     */
    private static String[] convertName(String legacyProgName)
    {
+      Function<String, ExternalProgram> lookupDirectName;
+      Function<String, ExternalProgram> lookupCompiledRcode;
+      Function<String, ExternalProgram> lookupAliasedDirectName;
+      Function<String, ExternalProgram> lookupAliasedCompiledRcode;
+      
+      Function<String, Boolean> isRcode = 
+         (pname -> pname.endsWith(".r") || (!caseSens && pname.endsWith(".R")));
+      boolean legacyRcode = isRcode.apply(legacyProgName);
+      boolean hasAliases = pathAliases != null;
+      String msg = "Found first match %s for program %s, and additional %s";
+      
+      lookupDirectName = (pname -> p2jMap.get(pname));
+      lookupCompiledRcode = !legacyRcode ? null : (pname ->
+      {
+         if (!isRcode.apply(pname))
+         {
+            return null;
+         }
+         
+         pname = pname.substring(0, pname.length() - ".r".length());
+         
+         ExternalProgram extProg = null;
+         
+         // we could use a trie, but keeping a match of names without extension to the full 
+         // program names (with extension) is easier
+         List<ExternalProgram> progs = p2jNonExtMap.get(pname);
+         
+         if (progs != null && !progs.isEmpty())
+         {
+            extProg = progs.get(0);
+
+            if (LOG.isLoggable(Level.WARNING))
+            {
+               for (int i = 1; i < progs.size(); i++)
+               {
+                  LOG.log(Level.WARNING, String.format(msg, 
+                                                       extProg.pname, 
+                                                       legacyProgName, 
+                                                       progs.get(i).pname));
+               }
+            }
+         }
+         
+         return extProg;
+      });
+      lookupAliasedDirectName = !hasAliases ? null : (pname ->
+      {
+         String[] aliases = resolvePathAliases(pname);
+         
+         ExternalProgram extProg = null;
+         
+         for (String aliasName : aliases)
+         {
+            ExternalProgram prog = lookupDirectName.apply(aliasName);
+            
+            if (prog != null)
+            {
+               if (extProg != null)
+               {
+                  if (LOG.isLoggable(Level.WARNING))
+                  {
+                     LOG.log(Level.WARNING, String.format(msg, 
+                                                          extProg.pname, 
+                                                          legacyProgName, 
+                                                          prog.pname));
+                  }
+               }
+               else
+               {
+                  extProg = prog;
+               }
+            }
+         }
+         
+         return extProg;
+      });
+      lookupAliasedCompiledRcode = !legacyRcode || !hasAliases ? null : (pname ->
+      {
+         if (!isRcode.apply(pname))
+         {
+            return null;
+         }
+         
+         String[] aliases = resolvePathAliases(pname);
+         
+         ExternalProgram extProg = null;
+         
+         for (String aliasName : aliases)
+         {
+            ExternalProgram prog = lookupCompiledRcode.apply(aliasName);
+            
+            if (prog != null)
+            {
+               if (extProg != null)
+               {
+                  LOG.log(Level.WARNING, String.format(msg, 
+                                                       extProg.pname, 
+                                                       legacyProgName, 
+                                                       prog.pname));
+               }
+               else
+               {
+                  extProg = prog;
+               }
+            }
+         }
+         
+         return extProg;
+      });
+      
+      @SuppressWarnings("unchecked")
+      Function<String, ExternalProgram>[] lookups = new Function[]
+      {
+         lookupDirectName,
+         lookupCompiledRcode,
+         lookupAliasedDirectName,
+         lookupAliasedCompiledRcode,
+      };
+      
+      for (Function<String, ExternalProgram> lookup : lookups)
+      {
+         if (lookup == null)
+         {
+            continue;
+         }
+
+         String[] res = convertName(legacyProgName, lookup);
+         if (res != null)
+         {
+            return res;
+         }
+      }
+      
+      return null;
+   }
+
+   /**
+    * Convert a Progress file name into Java class name. The input name can be an absolute file
+    * name, a file name relative to current location or a file name relative to some directory
+    * listed in the PROPATH variable. Before processing, the input file name is processed by
+    * {@link #canonicalize}.
+    * <p>
+    * A simple lookup is tried first. If this fails direct check for absolute path if attempted
+    * using one of the roots from {@code legacyRoots}. In multiple project scenario, the project
+    * name is also added as a path token in order to match the structure from
+    * {@code name_map.xml}.
+    * <p>
+    * If this mapping does not exist then each path segment of the PROPATH is tried in order. 
+    * The given program name is check to see if it is prefixed with the PROPATH segment, if so
+    * that prefix is removed and a lookup is done with this name. The first match found ends the
+    * search.
+    * <p>
+    * Absolute paths are converted into relative paths based on the existence of a matching path
+    * in the legacy PROPATH (see {@link EnvironmentOps#getLegacySearchPath}). To handle additional
+    * absolute paths, one must add the appropriate path prefix to the legacy PROPATH and it will
+    * be removed here.
+    * <p>
+    * This same PROPATH processing is how names relative to the current directory can be handled.
+    * This is the only way to handle that case since the current directory in the source system is
+    * runtime information that is not available here.
+    * <p>
+    * The legacy PROPATH is the key configuration tool by which one specifies to this method how
+    * to remove absolute and current directory relative prefixes.  This means that the legacy
+    * PROPATH that is used here may need to be different than the PROPATH on the original system.
+    * <p>
+    * As a fallback, if no mapping can be found and if the given {@code progName} is a fully
+    * qualified Java class name which references a valid Java class, then the {@code progName} is
+    * returned unchanged.
+    * <p>
+    * In Windows it can verify the absolute or relative program name is in current PROPATH 
+    * directory set.  This allows to properly map the following program names used in
+    * PROGRESS RUN statement:
+    * "c:/path/to/project/stcases/uast/rundir/absolute/program.p"
+    * "rundir/../../uast/rundir/relative/test11.p."
+    * "../uast/rundir/relative/test5.p"
+    *
+    * @param   legacyProgName
+    *          Progress file name.
+    * @param   lookup
+    *          The lookup function used to resolve a program name to a {@link ExternalProgram}.
+    *
+    * @return  A {@code String} array with two components: Java class name for the given Progress
+    *          file name as first element and the legacy procedure name in second element of the
+    *          array. If no appropriate mapping is present {@code null} is returned.
+    */
+   private static String[] convertName(String legacyProgName, 
+                                       Function<String, ExternalProgram> lookup)
+   {
       initMappingData();
       
       // get the original propath and split it into pieces
@@ -965,7 +1179,7 @@
             
             // regardless if there are '..' up paths or not, we must check the relName against
             // our dictionary - don't use the propath here.
-            extProg = p2jMap.get(relName);
+            extProg = lookup.apply(relName);
             
             if (extProg == null)
             {
@@ -974,7 +1188,7 @@
                String token = Utils.getProjectToken();
                if (token != null)
                {
-                  extProg = p2jMap.get(token + fileSep + relName);
+                  extProg = lookup.apply(token + fileSep + relName);
                }
             }
             
@@ -1022,7 +1236,7 @@
                // absolute folder.
 
                // check for a match
-               extProg = p2jMap.get(canonicalize(relName));
+               extProg = lookup.apply(canonicalize(relName));
                
                if (extProg != null)
                {
@@ -1038,7 +1252,7 @@
       // case 3. check the simple form of the name - in this case, just check for an exact match
       if (extProg == null)
       {
-         extProg = p2jMap.get(canonicalize(progName));
+         extProg = lookup.apply(canonicalize(progName));
       }
 
       // case 4. fallback processing
@@ -1217,6 +1431,99 @@
    }
    
    /**
+    * Given a program name, replace all folder names which have an associated 'source' folder,
+    * and return the new path.
+    * 
+    * @param    pname
+    *           The legacy program name.
+    *           
+    * @return   The resolved program name.
+    */
+   private static String[] resolvePathAliases(String pname)
+   {
+      String[] entries = pname.split(fileSep);
+      
+      // for now, we replace all found aliases with their source folders;  if needed, we can 
+      // compute all power sets made from pathAliases (will be 2^(pathAliases.size()) subsets)
+      
+      StringBuilder sb = new StringBuilder();
+      for (int i = 0; i < entries.length - 1; i++)
+      {
+         String entry = entries[i];
+         
+         // if this entry is an alias, get its source
+         String source = pathAliases.get(entry);
+         
+         sb.append(source == null ? entry : source).append(fileSep);
+      }
+      sb.append(entries[entries.length - 1]);
+      
+      return new String[] { sb.toString() };
+   }
+   
+   /**
+    * Load the path aliases from the directory, into the {@link #pathAliases} map.  This map 
+    * contains as key the 'aliased' name which is an alias for a 'source' folder.
+    * <p>
+    * The directory contains these mappings in the <code>/server/default/path-aliases</code> or
+    * <code>/server/&lt;server-id&gt;/path-aliases</code> node.  The child nodes have as class 
+    * <code>path-alias</code> and this structure:
+    * <code>
+    *     &lt;node class="path-alias" name="alias1"&gt;
+    *     <br>&nbsp;&nbsp;&nbsp;
+    *       &lt;node-attribute name="source" value="src1"/&gt;
+    *       <br>&nbsp;&nbsp;&nbsp;
+    *       &lt;node-attribute name="alias" value="obj1"/&gt;
+    *       <br>
+    *     &lt;/node&gt;
+    * </code>
+    * <p>
+    * The <code>alias1</code> name is just an unique ID for each child.
+    */
+   private static void loadPathAliases()
+   {
+      DirectoryService ds = DirectoryService.getInstance();
+      
+      if (!ds.bind())
+         throw new RuntimeException("directory bind failed");
+      
+      try
+      {
+         String path = Utils.findDirectoryNodePath(ds, "path-aliases", "container", false);
+         if (path == null)
+         {
+            return;
+         }
+         
+         String[] aliasPaths = ds.enumerateNodes(path);
+         if (aliasPaths.length == 0)
+         {
+            return;
+         }
+   
+         pathAliases = new HashMap<>();
+         for (String aliasPath : aliasPaths)
+         {
+            aliasPath = path + "/" + aliasPath;
+            String source = ds.getNodeString(aliasPath, "source");
+            String alias = ds.getNodeString(aliasPath, "alias");
+   
+            if (!caseSens)
+            {
+               source = source.toLowerCase();
+               alias = alias.toLowerCase();
+            }
+            
+            pathAliases.put(alias, source);
+         }
+      }
+      finally
+      {
+         ds.unbind();
+      }
+   }
+   
+   /**
     * This method loads all mapping data from a URL which is calculated
     * using the <code>pkgroot</code> and the <code>name_map.xml</code> with
     * all "." characters converted to file separators and an extra file
@@ -1243,6 +1550,7 @@
          try
          {
             p2jMap = new HashMap<>(1024);
+            p2jNonExtMap = new HashMap<>(1024);
             j2pMap = new HashMap<>(1024);
             
             // lookup the descriptors for the original file system
@@ -1344,7 +1652,24 @@
                ExternalProgram ep = buildExternalProgram(pname, jname, classMap);
                p2jMap.put(ep.pname, ep);
                j2pMap.put(ep.jname, ep);
+               
+               String nonExtName = pname;
+               int lastDotIdx = nonExtName.lastIndexOf('.');
+               if (lastDotIdx > 0)
+               {
+                  nonExtName = nonExtName.substring(0, lastDotIdx);
+               }
+               
+               List<ExternalProgram> progs = p2jNonExtMap.get(nonExtName);
+               if (progs == null)
+               {
+                  p2jNonExtMap.put(nonExtName, progs = new ArrayList<>(1));
+               }
+               
+               progs.add(ep);
             }
+            
+            loadPathAliases();
          }
          catch (Exception e)
          {

=== modified file 'src/dir_schema.xml'
--- src/dir_schema.xml	2017-04-02 10:19:33 +0000
+++ src/dir_schema.xml	2018-05-07 16:24:49 +0000
@@ -212,4 +212,8 @@
          <class-attribute name="account" type="STRING"  mandatory="false"  multiple="true" immutable="false" />
       </object-class>
       
+      <object-class name="path-alias" leaf="true" immutable="false">
+         <class-attribute name="source" type="STRING" mandatory="false" multiple="false" immutable="false" />
+         <class-attribute name="alias"  type="STRING" mandatory="false" multiple="false" immutable="false" />
+      </object-class>
 </schema-root>
\ No newline at end of file

