# SchemaStorage.java

    /*
    ** Module   : SchemaStorage.java
    ** Abstract : Contains the metadata that defines the directory schema.
    **
    ** Copyright (c) 2005-2025, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- --JPRM-- ----------------Description-----------------
    ** 001 SIY 20050418   @21033 Created initial version.
    ** 002 GES 20090515   @42208 Imports change.
    ** 003 GES 20101116          Allow schema loading from multiple XML input
    **                           sources. These sources can be resources in a JAR
    **                           or files in the filesystem.  The objective is to
    **                           always obtain the core directory schema from a
    **                           standard source that is part of the project, while
    **                           still allowing custom extensions of the schema to
    **                           be added in a specific implementation.
    ** 004 EVL 20160225          Javadoc fixes to make compatible with Oracle Java 8 for Solaris 10.
    ** 005 SP  20250416          Used CaseInsensitiveHashMap for list map. 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.util.*;
    import java.net.*;
    import javax.naming.directory.*;
    import org.w3c.dom.*;

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

    /**
     * Holds the set of directory object definitions (instances of
     * {@link ObjectClass}) which represent the schema (structure) of the directory.
     */
    class SchemaStorage
    {
       /** XML attribute for the attribute "multiple" flag. */
       private static final String ATTR_MULT = "multiple";

       /** XML attribute for the attribute "name" field. */
       private static final String ATTR_NAME = "name";

       /** XML attribute for the attribute "mandatory" flag. */
       private static final String ATTR_MAND = "mandatory";

       /** XML attribute for the attribute "read-only" flag. */
       private static final String ATTR_RO = "immutable";

       /** XML attribute for the attribute "type" field. */
       private static final String ATTR_TYPE = "type";

       /** XML attribute for the class "leaf" flag. */
       private static final String ATTR_LEAF = "leaf";

       /** XML tag for the class element. */
       private static final String ELEM_CLASS = "object-class";

       /** XML tag for the class attribute element. */
       private static final String ELEM_CLASS_ATTR = "class-attribute";

       /** Singleton instance. */
       private static SchemaStorage schema = null;

       /** Directory object definitions (loaded once at startup and not changed). */
       private Map<String, ObjectClass> list = new CaseInsensitiveHashMap<>();

       /** Cached list of the keys in our map. */
       private String[] keys = null;

       /**
        * Construct an instance.  This is private to enforce the singleton pattern.
        *
        * @param    urls
        *           List of XML resources from which the schema will be read.  The
        *           order of the array defines the order of loading.
        * @param    required
        *           List of object classes that must exist in the loaded schema.
        */
       private SchemaStorage(URL[] urls, String[] required)
       throws Exception
       {
          load(urls);
          checkRequiredClasses(required);
          initKeys();
       }

       /**
        * Get the singleton instance.
        *
        * @return   Singleton instance.
        */
       static synchronized SchemaStorage getSchema()
       {
          return schema;
       }

       /**
        * Initialize the singleton instance.
        *
        * @param    urls
        *           List of XML resources from which the schema will be read.  The
        *           order of the array defines the order of loading.
        * @param    required
        *           List of object classes that must exist in the loaded schema.
        *
        * @return   The singleton instance.
        *
        * @throws   Exception
        *           If an error occurred during schema initialization.
        */
       static synchronized SchemaStorage initSchema(URL[] urls, String[] required)
       throws Exception
       {
          if (schema == null)
          {
             schema = new SchemaStorage(urls, required);
          }
          return schema;
       }

       /**
        * Get list of class names.
        *
        * @return   An array of names of the defined object classes
        */
       String[] getClassNames()
       {
          return keys;
       }

       /**
        * Get definition for the specified Object Class.
        *
        * @param    name
        *           Name of the Object Class to locate.
        *
        * @return   Reference to <code>ObjectClass</code> or <code>null</code>
        *           if no such class defined.
        */
       ObjectClass getObjectClass(String name)
       {
          return list.get(name);
       }

       /**
        * Read the schema definitions and create the list of object classes.
        *
        * @param    urls
        *           List of XML resources from which the schema will be read.  The
        *           order of the array defines the order of loading.
        */
       private void load(URL[] urls)
       throws Exception
       {
          for (URL url : urls)
          {
             loadWorker(url);
          }

          // TODO: why is this here? If needed, it should be added somewhere else.
          // add the special "ldapNode"
          {
             ObjectClass obj = new ObjectClass("ldapNode", false, false,
                new AttributeDefinition[]
                {
                   /* name, type, mandatory, multiple, immutable */
                   new AttributeDefinition("location", AttributeType.ATTR_STRING,
                                           true, false, false),
                   new AttributeDefinition("p2jclass", AttributeType.ATTR_STRING,
                                           true, false, false),
                });

             list.put(obj.getName(), obj);
          }
       }

       /**
        * Read the given schema definition and add to the list of object classes.
        *
        * @param    url
        *           XML resource from which the schema will be read.
        */
       private void loadWorker(URL url)
       throws Exception
       {
          Document dom = XmlHelper.parse(url.openStream());
          Element elem = dom.getDocumentElement();
          Node rootNode = elem.getFirstChild();

          if (rootNode == null)
          {
             String spec = "Missing root element in the XML file (%s).";
             throw new ConfigurationException(String.format(spec, url));
          }

          while (rootNode != null)
          {
             if (rootNode.getNodeName().equals(ELEM_CLASS))
             {
                elem = (Element)rootNode;

                String strName = elem.getAttribute(ATTR_NAME);
                String strLeaf = elem.getAttribute(ATTR_LEAF);
                String strRO = elem.getAttribute(ATTR_RO);

                if (strName == null || strLeaf == null || strRO == null)
                {
                   continue;
                }

                NodeList attr = rootNode.getChildNodes();

                Vector<AttributeDefinition> attrs = new Vector<>();
                AttributeDefinition[] lst = new AttributeDefinition[0];

                for (int j = 0; j < attr.getLength(); j++)
                {
                   if (attr.item(j).getNodeName().equals(ELEM_CLASS_ATTR))
                   {
                      elem = (Element)attr.item(j);
                      String attrName = elem.getAttribute(ATTR_NAME);
                      String attrRO = elem.getAttribute(ATTR_RO);
                      String attrMult = elem.getAttribute(ATTR_MULT);
                      String attrReq = elem.getAttribute(ATTR_MAND);
                      String attrType = elem.getAttribute(ATTR_TYPE);

                      attrs.add(new AttributeDefinition(attrName,
                                                        str2type(attrType),
                                                        str2bool(attrReq),
                                                        str2bool(attrMult),
                                                        str2bool(attrRO)));
                   }
                }
                if (attrs.size() > 0)
                {
                   lst = attrs.toArray(lst);
                }
                else
                {
                   lst = null;
                }

                ObjectClass obj = new ObjectClass(strName, str2bool(strLeaf), str2bool(strRO), lst);
                list.put(strName, obj);
             }

             rootNode = rootNode.getNextSibling();
          }
       }

       /**
        * Check resulting schema for the presence of required classes.
        *
        * @param    required
        *           List of object classes that must exist in the loaded schema.
        *
        * @throws   ConfigurationException
        *           In case if not all required classes are present in the schema.
        */
       private void checkRequiredClasses(String[] required)
       throws ConfigurationException
       {
          for (int i = 0; i < required.length; i++)
          {
             if (!list.containsKey(required[i]))
             {
                throw new ConfigurationException("Schema definition lacks " +
                                                 " required " + required[i] +
                                                 " object class.");
             }
          }
       }

       /**
        * Initialize the cached list of keys in our map.  Since the keys don't
        * change once the map is loaded, there is no reason to calculate the
        * list dynamically.  Instead, it is done once at initialization time
        * and the results are saved off.  This increases runtime performance.
        */
       private void initKeys()
       {
          keys = new String[list.size() - 1];

          int i = 0;
          for (String key : list.keySet())
          {
             // TODO: why is this here?
             // hide the special ldap node key
             if ("ldapNode".equals(key))
             {
                continue;
             }
             keys[i++] = key;
          }
       }

       /**
       * Convert type name into type code using {@link AttributeType#fromString}.
        *
        * @param    type
        *           Type name.
        *
        * @return   Type code restored from type name.
        *
        * @throws   InvalidAttributesException
        *           If no such type name exists.
        */
       private int str2type(String type)
       throws InvalidAttributesException
       {
          int res = AttributeType.fromString(type);
          if (res == 0)
          {
             throw new InvalidAttributesException("Invalid attribute type [" + type + "]");
          }
          return res;
       }

       /**
        * Convert string into boolean value. Only a string which contains text
        * "true" (case does not matter) is converted into <code>true</code>.
        *
        * @param    str
        *           Source string.
        *
        * @return   Converted boolean value from the given string.
        */
       private static boolean str2bool(String str)
       {
          return Boolean.valueOf(str).booleanValue();
       }
    }