# 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);
          }
       }
    }