LdapMapGen.java

/*
** Module   : LdapMapGen.java
** Abstract : Mapping generator for LDAP back-end.  
**
** Copyright (c) 2005-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- ----------------Description-----------------
** 001 SIY 20050404   @21015 Created initial version
** 002 SIY 20050505   @21195 Moved method upcaseFirst() to IdUtils, 
**                           updated documentation, compressed imports.
** 003 GES 20090515   @42207 Imports change.
** 004 GES 20101117          Switched to new static schema loader that reads
**                           from well-known resources rather than a manually
**                           defined configuration file.
** 005 TJD 20220504          Java 11 compatibility minor changes 
** 006 GBB 20230512          Logging methods replaced by CentralLogger/ConversionStatus.
** 007 SP  20250417          Code formatting and javadoc fixes.
*/

/*
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU Affero General Public License as
** published by the Free Software Foundation, either version 3 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 Affero General Public License for more details.
**
** You may find a copy of the GNU Affero GPL version 3 at the following
** location: https://www.gnu.org/licenses/agpl-3.0.en.html
** 
** Additional terms under GNU Affero GPL version 3 section 7:
** 
**   Under Section 7 of the GNU Affero GPL version 3, the following additional
**   terms apply to the works covered under the License.  These additional terms
**   are non-permissive additional terms allowed under Section 7 of the GNU
**   Affero GPL version 3 and may not be removed by you.
** 
**   0. Attribution Requirement.
** 
**     You must preserve all legal notices or author attributions in the covered
**     work or Appropriate Legal Notices displayed by works containing the covered
**     work.  You may not remove from the covered work any author or developer
**     credit already included within the covered work.
** 
**   1. No License To Use Trademarks.
** 
**     This license does not grant any license or rights to use the trademarks
**     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
**     of Golden Code Development Corporation. You are not authorized to use the
**     name Golden Code, FWD, or the names of any author or contributor, for
**     publicity purposes without written authorization.
** 
**   2. No Misrepresentation of Affiliation.
** 
**     You may not represent yourself as Golden Code Development Corporation or FWD.
** 
**     You may not represent yourself for publicity purposes as associated with
**     Golden Code Development Corporation, FWD, or any author or contributor to
**     the covered work, without written authorization.
** 
**   3. No Misrepresentation of Source or Origin.
** 
**     You may not represent the covered work as solely your work.  All modified
**     versions of the covered work must be marked in a reasonable way to make it
**     clear that the modified work is not originating from Golden Code Development
**     Corporation or FWD.  All modified versions must contain the notices of
**     attribution required in this license.
*/

package com.goldencode.p2j.directory;

import java.io.*;
import java.security.InvalidParameterException;
import java.util.*;
import java.util.logging.*;

import javax.naming.*;
import javax.naming.directory.*;

import com.goldencode.p2j.util.logging.*;
import org.w3c.dom.*;

import com.goldencode.util.*;
import com.goldencode.p2j.cfg.ConfigurationException;

/**
 * The design of the LDAP back-end allows different modes of operation but all
 * of them based on the mapping information collected in some way. The mapping
 * information tells LDAP back-end how to represent LDAP server to P2J
 * DirectoryService users.
 * <p>
 * The mapping contains two main parts: <b>schema-level </b> mapping and
 * <b>node-level </b> mapping. The <b>schema-level </b> mapping defines how
 * particular LDAP object class and its attributes corresponds to P2J object
 * classes and attributes. The <b>node-level </b> mapping defines which LDAP
 * node represents particular node in P2J directory.
 * <p>
 * The LDAP back-end supports two major modes of operation: when only
 * schema-level mapping is defined and when both schema-level and node-level
 * mapping are provided. The difference between them is that first one assumes
 * that LDAP server already provides hierarchy suitable for use by P2J and
 * LDAP back-end just uses it as is. In second mode there is no need to have
 * such a hierarchy, each P2J node can be individually mapped into LDAP node
 * and some nodes may not have corresponding LDAP node at all.
 * <p>
 * Since writing mapping information manually is complicated and error prone
 * procedure, <code>LdapMapGen</code> utility is provided to simplify it.
 * The main purpose of the <code>LdapMapGen</code> utility is to generate
 * mapping information required for use of P2J DirectoryService subsystem with
 * LDAP server. The resulting mapping information can be stored in the map
 * file in XML format recognized by LDAP back-end or at the LDAP server in
 * specified location. Beside that, utility in some cases can generate schema
 * file suitable for use with LDAP server.
 */
public class LdapMapGen
{
   /** Logger */
   private static final CentralLogger LOG = CentralLogger.get(LdapMapGen.class);
   
   /** One-to-one mapping between LDAP and P2J directories */
   private static final int MAP_SUBTREE = 1;

   /** Node-to-node mapping */
   private static final int MAP_NODE = 2;
   
   /** Command line parameters */
   private String[] args;

   /**
    * Mapping between LDAP and P2J nodes as it defined in configuration file.
    */
   private Map<String, String> configNodeList = new CaseInsensitiveHashMap<>();

   /** LDAP directory context */
   private DirContext context = null;

   /** Configuration parameter: LDAP credentials */
   private String ldapCredentials = null;

   /** Configuration parameter: LDAP principal */
   private String ldapPrincipal = null;

   /** Configuration parameter: LDAP URL */
   private String ldapUrl = null;

   /** Mapping data */
   private SchemaMapping mapping = new SchemaMapping();

   /** Mapping mode between LDAP and P2J */
   private int mappingMode = MAP_SUBTREE;

   /** Complete expanded mapping between P2J and LDAP nodes */
   private Map<String, String> nodeList = new CaseInsensitiveHashMap<>();

   /** Heading text for the LDAP schema*/
   private String ldapSchemaHeader;

   /** Root of the mapping if saving to subtree is desired */
   private String mappingNode = null;

   /** LDAP node attribute name where mapping will be stored */
   private String mapAttribute = null;

   /** LDAP object class of the node where mapping will be stored */
   private String mapObjClass = null;

   /**
    * Make LDAP attribute name from object class name and attribute name.
    * 
    * @param   cls
    *          Object class name.
    * @param   attr
    *          Attribute name.
    *
    * @return  LDAP attribute name.
    */
   private static String mkAttrName(String cls, String attr)
   {
      return "gcd" + IdUtils.upcaseFirst(cls) + IdUtils.upcaseFirst(attr);      
   }

   /**
    * Sort array of strings.
    * 
    * @param   src
    *          Source array.
    *
    * @return  Sorted array.
    */
   private static String[] sortArray(String[] src)
   {
      if (src == null)
      {
         return null;
      }
      if (src.length == 0)
      {
         return src;
      }
      
      Map<String, String> map = new HashMap<>();
      for (int i = 0; i < src.length; i++)
      {
         map.put(src[i], src[i]);
      }
      
      Set<String> set = new TreeSet<>(map.keySet());
      String[] res = new String[src.length];
      
      int i = 0;
      
      for (Iterator<String> iter = set.iterator(); iter.hasNext(); i++)
      {
         res[i] = new String(iter.next());
      }

      return res;
   }

   /**
    * Build list of attributes from the vector.
    * 
    * @param   req
    *          Vector with attribute names.
    *
    * @return  String containing attribute names from the vector.
    */
   private static String vec2str(List<String> req)
   {
      if (req.size() == 1)
      {
         return req.get(0);
      }
      
      StringBuilder res = new StringBuilder("(");
      for (int i = 0; i < req.size(); i++)
      {
         res.append(req.get(i));
         if (i != (req.size() - 1))
         {
            res.append(" $ ");
         }
      }
      
      return res + ")";
   }

   /**
    * Shortcut for the System.out.prinln()
    * 
    * @param   msg
    *          Message to print to standard output.
    */
   static void say(String msg)
   {
      System.out.println(msg);
   }

   /**
    * Convert string into boolean value. Only string which contains text
    * "true" (case does not matter) is converted into boolean
    * <code>true</code>
    * 
    * @param   str
    *          Source string.
    *
    * @return  <code>boolean</code> value for given string.
    */
   static boolean str2bool(String str)
   {
      return Boolean.valueOf(str).booleanValue();
   }
   
   /**
    * Construct an instance for specified command line parameters.
    * 
    * @param   args
    *          Command line parameters passed to main()
    */
   LdapMapGen(String[] args)
   {
      this.args = args;
   }
   
   /**
    * Create node of "ldapNode" type for specified P2J node and class.
    * 
    * @param   remapper
    *          XML back-end reference.
    * @param   nodeId
    *          P2J node ID.
    *
    * @throws  NamingException
    *          Forwarded from <code>getNodeClass()</code>.
    */
   void createNode(RamRemapper remapper, String nodeId)
   throws NamingException
   {
      String ldapClass = getNodeClass(nodeId);
      String p2jClass  = mapping.getP2jClass(ldapClass);
      String cn        = nodeList.get(nodeId);

      if (p2jClass == null)
      {
         say("No mapping defined for the object class " + ldapClass + " found in node " + nodeId);
         return;
      }

      //say("Creating node for class " + ldapClass);
      if (mappingMode != MAP_SUBTREE)
      {
         //Check presence of intermediate nodes
         String[] paths = IdUtils.listPaths(nodeId);
         
         for (int i = 0; paths != null && i < paths.length; i++)
         {
            if (remapper.getNodeClassName(paths[i]) == null)
            {
               remapper.addNode(paths[i], "container", null, null);
            }
         }
      }
      
      //say("P2J address = " + nodeId);

      boolean rc = true;
      
      if (nodeId.equals("/") && mappingMode != MAP_SUBTREE)
      {
         replaceRoot(remapper, cn, p2jClass);
      }
      else
      {
         rc = remapper.addNode(nodeId, "ldapNode",
                               new String[] {"location", "p2jclass"},
                               new String[] {cn, p2jClass});
      }

      if (!rc)
      {
         say("Creating of node [id:" + nodeId + ", location: " + cn + " class: " + ldapClass + "] failed");
      }
   }

   /**
    * Expand all subtree to subtree mappings into explicit node-to-node
    * mappings.
    * 
    * @throws  NamingException
    *          Forwarded from <code>expandWildcard()</code> method.
    */
   void expandNodeMappings()
   throws NamingException
   {
      String[] keys;
      keys = configNodeList.keySet().toArray(new String[0]);

      for (int i = 0; i < keys.length; i++)
      {
         if (keys[i].endsWith("*"))
         {
            //say("Expanding " + keys[i]);
            
            expandWildcard(nodeList, keys[i], configNodeList.get(keys[i]));
         }
         else
         {
            //say("Adding " + keys[i]);
            nodeList.put(configNodeList.get(keys[i]), keys[i]);
         }
      }
   }

   /**
    * Expand subtree node ID with wildcards and return a list of all nodes in
    * the subtree.
    * 
    * @param   result
    *          Storage for the node to node mapping.
    * @param   cn
    *          LDAP node ID.
    * @param   nodeId
    *          P2J node ID
    *
    * @throws  NamingException
    *          Forwarded from the JNDI methods.
    */
   void expandWildcard(Map<String, String> result, String cn, String nodeId)
   throws NamingException
   {
      //Strip mask
      if (cn.endsWith("*"))
      {
         cn = cn.substring(0, cn.length() - 1);
      }

      expandWorker(result, prepareContext(), cn, nodeId);
   }

   /**
    * Recursive worker routine for the <code>expandWildcard()</code> method.
    * 
    * @param   result
    *          Vector with collected nodes.
    * @param   ctx
    *          LDAP Directory context to scan.
    * @param   cn
    *          Node ID to start scan from.
    * @param   nodeId
    *          P2J node ID.
    *
    * @throws  NamingException
    *          Forwarded from <code>DirContext.list()</code> method.
    */
   void expandWorker(Map<String, String> result, DirContext ctx, String cn, String nodeId)
   throws NamingException
   {
      NamingEnumeration<NameClassPair> list = ctx.list(cn);

      if (cn.length() > 0)
      {
         result.put(nodeId, cn);
      }

      while (list.hasMore())
      {
         NameClassPair nc = list.next();

         String name = nc.getName();

         String[] lst = name.split("=", 2);

         if (lst == null || lst.length != 2)
         {
            say("Unable to parse LDAP node name " + name + " at " + cn);
            continue;
         }

         String newId = nodeId + "/" + lst[1];

         if (cn.length() > 0)
         {
            name = name + "," + cn;
         }

         expandWorker(result, ctx, name, newId);
      }
   }

   /**
    * Return LDAP Object Class name for specified P2J node.
    * 
    * @param   nodeId
    *          LDAP node ID.
    * @return  Object Class for specified LDAP node.
    *
    * @throws  NamingException
    *          Forwarded from the JNDI classes.
    */
   String getNodeClass(String nodeId)
   throws NamingException
   {
      String cn = nodeList.get(nodeId);
      if (cn == null)
      {
         return null;
      }

      Attributes answer;
      answer = prepareContext().getAttributes(cn, new String[]
         {
            "objectClass"
         });

      return (String)answer.get("objectClass").get();
   }

   /**
    * Load map configuration file.
    * 
    * @param   file
    *          Source file name.
    * @throws  Exception
    *          Forwarded from the various classes used my method.
    */
   void loadConfig(String file)
   throws Exception
   {
      Document dom = XmlHelper.parse(new File(file));

      Element elem = dom.getDocumentElement();

      Node rootNode = elem.getFirstChild();

      if (rootNode == null)
      {
         throw new ConfigurationException("Invalid configuration file" + file);
      }

      while (rootNode != null)
      {
         if (rootNode.getNodeName().equals("config"))
         {
            elem = (Element)rootNode;
            
            String type = elem.getAttribute("mapping-mode");

            if ("subtree".equalsIgnoreCase(type) ||
                "direct".equalsIgnoreCase(type))
            {
               mappingMode = MAP_SUBTREE;
            }
            else if ("node".equalsIgnoreCase(type))
            {
               mappingMode = MAP_NODE;
            }
            else
            {
               say("Unknown mapping mode [" + type + "]");
            }

            ldapPrincipal = elem.getAttribute("principal");
            ldapCredentials = elem.getAttribute("credentials");
            ldapUrl = elem.getAttribute("url");
            
            // this element is no longer supported, instead the standard
            // schema and any schema extensions will be loaded as is done in
            // the normal server startup
            // schemaFile = elem.getAttribute("p2j-schema-file");
            
            ldapSchemaHeader = elem.getAttribute("ldap-schema-header");

            mappingNode = elem.getAttribute("mapping-destination");
            mapAttribute = elem.getAttribute("mapping-attribute");
            mapObjClass = elem.getAttribute("mapping-object-class");
         }
         else if (rootNode.getNodeName().equals("node-mapping"))
         {
            NodeList attr = rootNode.getChildNodes();

            for (int i = 0; i < attr.getLength(); i++)
            {
               if (attr.item(i).getNodeName().equals("node"))
               {
                  elem = (Element)attr.item(i);
                  String ldap = elem.getAttribute("ldap");
                  String p2j = elem.getAttribute("p2j");

                  if (ldap == null || p2j == null)
                  {
                     say("Invalid mapping [" + ldap + "<->" + p2j
                        + "] ignored");
                     continue;
                  }

                  configNodeList.put(ldap, p2j);
               }
            }
         }
         else if (rootNode.getNodeName().equals("class-mapping"))
         {
            NodeList attr = rootNode.getChildNodes();

            for (int i = 0; i < attr.getLength(); i++)
            {
               if (attr.item(i).getNodeName().equals("class"))
               {
                  Element attrElem = (Element)attr.item(i);
                  String ldapClass = attrElem.getAttribute("ldap");
                  String p2jClass = attrElem.getAttribute("p2j");

                  if (ldapClass == null || p2jClass == null)
                  {
                     say("Invalid mapping [" + ldapClass + "<->" + p2jClass + "] ignored");
                     continue;
                  }

                  if (ldapClass.isEmpty() && p2jClass.isEmpty())
                  {
                     continue;
                  }

                  mapping.mapP2jClassToLdap(p2jClass, ldapClass);

                  NodeList attrMap = elem.getChildNodes();

                  for (int j = 0; j < attrMap.getLength(); j++)
                  {
                     if (attrMap.item(j).getNodeName().equals("attribute"))
                     {
                        Element mapElem = (Element)attrMap.item(j);
                        
                        String ldapAttr = mapElem.getAttribute("ldap");
                        String p2jAttr = mapElem.getAttribute("p2j");

                        if (ldapAttr == null || p2jAttr == null)
                        {
                           say("Invalid mapping [" + ldapClass + "." + ldapAttr +
                               "<->" + p2jClass + "." + p2jAttr + "] ignored");
                           continue;
                        }
                        
                        if (ldapAttr.isEmpty() || p2jAttr.isEmpty())
                           continue;
                        
                        mapping.mapAttribute(p2jClass, p2jAttr, ldapClass,
                                             ldapAttr);
                     }
                  }
               }
            }
         }
         
         rootNode = rootNode.getNextSibling();
      }
   }

   /**
    * Prepare DirContext using LDAP configuration parameters obtained from
    * configuration file.
    * 
    * @return  Prepared LDAP context.
    * @throws  NamingException
    *          Forwarded from <code>InitialDirContext()</code>.
    */
   DirContext prepareContext()
   throws NamingException
   {
      if (context != null)
         return context;

      Hashtable<String,String> env = new Hashtable<>();

      env.put(Context.INITIAL_CONTEXT_FACTORY,
              "com.sun.jndi.ldap.LdapCtxFactory");

      env.put(Context.PROVIDER_URL, ldapUrl);

      env.put(Context.SECURITY_AUTHENTICATION, "simple");
      env.put(Context.SECURITY_PRINCIPAL, ldapPrincipal);
      env.put(Context.SECURITY_CREDENTIALS, ldapCredentials);

      context = new InitialDirContext(env);

      return context;
   }

   /**
    * Main application worker.
    */
   void run()
   {
      //Parse arguments

      //Assume first parameter is file name

      String configFile = null;
      String outputFile = null;
      String ldapSchema = null;
      boolean updateLdap = false;
      
      for (int i = 0; i < args.length; i++)
      {
         if (i == 0)
         {
            configFile = args[0];
            continue;
         }
         
         if (args[i].equalsIgnoreCase("-o"))
         {
            if (args.length > (i + 1))
               outputFile = args[++i];
            continue;
         }
         
         if (args[i].equalsIgnoreCase("-s"))
         {
            if (args.length > (i + 1))
               ldapSchema = args[++i];
            continue;
         }

         if (args[i].equalsIgnoreCase("-u"))
         {
            updateLdap = true;
            outputFile = "";
            continue;
         }
      }
      
      if (configFile == null || (outputFile == null && ldapSchema == null))
      {
         say("Usage: java LdapMapGen <source_map.xml> "
            + " [-o <output_map.xml> | -u]"
            + " [-s <ldap.schema>]");
         return;
      }

      try
      {
         loadConfig(configFile);

         SchemaLoad.initSchema();
         
         //Check if SchemaMapping contains anything
         
         String[] list = mapping.getMapList();
         
         if (list == null || list.length == 0)
            selfMap();
         else
         {
            if (ldapSchema != null)
               say("Warning: generation of LDAP schema is supported " +
                   "only from P2J schema file. Request ignored.");
         }
         
         mapping.checkRequiredClasses();
         
         if (updateLdap)
         {
            say("Writing schema mapping to " + mappingNode);
            writeLdap();
            say("Done.");
         }
         else
         {
            if (outputFile != null)
            {
               if (mappingMode == MAP_SUBTREE)
                  configNodeList.put("*", "");
               
               expandNodeMappings();
               
               say("Writing map to " + outputFile);
               writeMap(outputFile);
               say("Done.");
            }
         }
         
         if (ldapSchema != null)
         {
            say("Writing LDAP schema to " + ldapSchema);
            writeLdapSchema(ldapSchema);
            say("Done.");
         }
      }
      catch (Exception e)
      {
         if (e instanceof FileNotFoundException)
            throw new InvalidParameterException(e.getMessage());
         LOG.log(Level.FINE, "", e);
      }
   }

   /**
    * Write resulting XML map file.
    * 
    * @param   file
    *          XML file name.
    * @throws  Exception
    *          Forwarded from <code>createNode()</code> method.
    */
   void writeMap(String file)
   throws Exception
   {
      RamRemapper remapper = new RamRemapper();

      remapper.bind();
      
      Set<String> nodes = new TreeSet<>(nodeList.keySet());

      for (Iterator<String> iter = nodes.iterator(); iter.hasNext();)
      {
         String nodeId = iter.next();
         createNode(remapper, nodeId);
      }

      XmlRemapperIO io = new XmlRemapperIO(remapper, mapping);

      //Build root node manually
      if (mappingMode == MAP_SUBTREE)
      {
         replaceRoot(remapper, "", "container");
      }
      
      io.save(new FileOutputStream(file));

      remapper.unbind();
   }

   /**
    * Replace root node with one of type "ldapNode".
    * 
    * @param   remapper
    *          Back-end to perform operation on.
    * @param   cn
    *          LDAP node location.
    * @param   p2jClass
    *          Name of the P2J Object Class.
    */
   private void replaceRoot(RamRemapper remapper, String cn, String p2jClass)
   {
      SchemaStorage schema = SchemaStorage.getSchema();
      
      RamNode r = new RamNode(schema.getObjectClass("ldapNode"), "");

      r.addStringAttributeValue("location", cn);
      r.addStringAttributeValue("p2jclass", p2jClass);
      
      RamNode[] list = remapper.getRoot().getChildList();
      
      for (int i = 0; i < list.length; i++)
      {
         r.addChild(list[i]);
      }
      
      remapper.setRoot(r);
   }

   /**
    * Write LDAP schema file into
    * 
    * @param   outFile
    *          File name where LDAP schema will be saved.
    */
   private void writeLdapSchema(String outFile)
   {
      try
      {
         PrintStream out = new PrintStream(new FileOutputStream(outFile));
         
         //Output header
          
         if (ldapSchemaHeader != null)
         {
            out.println(ldapSchemaHeader);
         }
         
         int order = 100;
         
         SchemaStorage schema = SchemaStorage.getSchema();
         
         String[] classList = sortArray(schema.getClassNames());
         
         for (int i = 0; i < classList.length; i++)
         {
            ObjectClass objClass = schema.getObjectClass(classList[i]);
            if (objClass.getName().equals("metaclass"))
            {
               continue;
            }
            
            AttributeDefinition[] attrs = objClass.getClassDefinition();
            
            for (int j = 0; attrs != null && j < attrs.length; j++)
            {
               String[] typeStr =       
               {
                  "unknown" , "Integer"    , "Boolean"  ,
                  "String"  , "Double"     , "ByteArray", 
                  "BitField", "BitSelector", "Date"     ,
                  "Time"
               };

               //Skip Double, there is no appropriate mapping for it yet.
               //To-Do.
               if (attrs[j].getType() == AttributeType.ATTR_DOUBLE)
               {
                  continue;
               }

               String type = typeStr[attrs[j].getType()];
               if (attrs[j].isMultiple())
               {
                  type += 's';
               }
               
               out.println("attributetype ( GCD.P2J.At:" + order+ " NAME " +
                   "'" + mkAttrName(objClass.getName(), attrs[j].getName()) +
                   "' SUP gcd" + type + ")");
               
               order++;
            }
            
            
         }
         
         out.println("\n#---- object classes --------------------------------------------------------");
         
         out.println();
         
         order = 1;         
         
         for (int i = 0; i < classList.length; i++)
         {
            ObjectClass objClass = schema.getObjectClass(classList[i]);
            String name = objClass.getName();
            if (name.equals("metaclass"))
            {
               continue;
            }

            String cn = "cn";
            if (name.equals("user"))
            {
               cn = "uid";
            }
            
            out.println();
            out.println("# " + name);
            out.println("objectclass ( GCD.P2J.OC:" + order + " NAME 'gcd" + IdUtils.upcaseFirst(name) +
                        "'\n" + "  DESC 'P2J " + name + " class'\n" + "  SUP top STRUCTURAL");
            
            order++;
            
            AttributeDefinition[] attrs = objClass.getClassDefinition();
            
            List<String> opt = new ArrayList<>();
            List<String> req = new ArrayList<>();
            
            req.add(cn);
            
            for (int j = 0; attrs != null && j < attrs.length; j++)
            {
               String attr = mkAttrName(name, attrs[j].getName());
               if (attrs[j].isMandatory())
               {
                  req.add(attr);
               }
               else
               {
                  opt.add(attr);
               }
            }

            String list = vec2str(req);
            if (opt.size() == 0)
            {
               out.println("  MUST " + list + ")");
            }
            else
            {
               out.println("  MUST " + list);
               list = vec2str(opt);
               out.println("  MAY " + list + ")");
            }
            
            out.println();
         }
         out.println("## end of schema");
         out.close();
      }
      catch(Exception e)
      {
         say("Error writing mapping file.");
         LOG.log(Level.FINE, "", e);
      }
   }
   
   /**
    * Generate mapping using only P2J schema data. 
    */
   private void selfMap()
   {
      SchemaStorage schema = SchemaStorage.getSchema();
      String[] classList = sortArray(schema.getClassNames());
      
      for (int i = 0; i < classList.length; i++)
      {
         ObjectClass objClass = schema.getObjectClass(classList[i]);
         String p2jClass = objClass.getName();
         
         if (p2jClass.equals("metaclass"))
         {
            continue;
         }
         
         String cn = "cn";
         
         String ldapClass = "gcd" + IdUtils.upcaseFirst(p2jClass);

         if (p2jClass.equals("user"))
         {
            cn = "uid";
         }
         
         mapping.mapLdapClassToP2jClass(ldapClass, p2jClass);
         mapping.mapAttribute(p2jClass, "objectName", ldapClass, cn);
         
         AttributeDefinition[] attrs = objClass.getClassDefinition();

         for (int j = 0; attrs != null && j < attrs.length; j++)
         {
            mapping.mapAttribute(p2jClass, attrs[j].getName(), ldapClass, 
                                 mkAttrName(p2jClass, attrs[j].getName()));
         }
      }
   }

   /**
    * Write mapping into LDAP node/attribute.
    * 
    * @throws  ConfigurationException
    *          In case of configuration error.
    */
   private void writeLdap()
   throws ConfigurationException
   {
      if (mappingNode == null)
      {
         throw new ConfigurationException("LDAP mapping node is not defined");
      }
      if (mapAttribute == null)
      {
         throw new ConfigurationException("LDAP mapping node attribute is not defined");
      }
      if (mapObjClass == null)
      {
         throw new ConfigurationException("LDAP mapping node object class is not defined");
      }
      try
      {
         DirContext ctx = prepareContext();

         Attributes attrs = new BasicAttributes(true);
         BasicAttribute attr = new BasicAttribute("objectClass", mapObjClass);

         attrs.put(attr);

         attr = new BasicAttribute(mapAttribute);

         String[] map = mapping.getMapList();

         for (int i = 0; i < map.length; i++)
         {
            attr.add(map[i]);
         }

         attrs.put(attr);
         
         try
         {
            ctx.createSubcontext(mappingNode, attrs);
         }
         catch (Exception e)
         {
            attrs.remove("objectClass");
            
            ctx.modifyAttributes(mappingNode, DirContext.REPLACE_ATTRIBUTE, attrs);
         }
         
         System.out.println("Total " + map.length + " mappings written");
      }
      catch (NamingException e)
      {
         throw new ConfigurationException("Unable to create LDAP node");
      }
   }

   /**
    * Command line driver.
    * 
    * @param   args
    *          Application command line parameters. File name is the only one
    *          expected.
    */
   public static void main(String[] args)
   {
      try
      {
         LdapMapGen worker = new LdapMapGen(args);

         worker.run();
      }
      catch (Exception e)
      {
         LOG.severe("", e);
      }
   }
}