DirNode.java

/*
** Module   :DirNode.java
** Abstract :Directory node data container.
**
** Copyright (c) 2005-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- ----------------Description-----------------
** 001 SIY 20050508  @21193  Created initial version (extracted from 
**                           RamNode)
** 002 NVS 20060426  @25725  Added protection against wrong class names
** 003 SIY 20090712  @43139  Fixed method name. Inferred generics, refactoring.
**                           Added hasChild() method.
** 004 SIY 20091007  @44127  Replaced HashMap with LinkedHashMap in attributes
**                           and child nodes storage. This significantly 
**                           reduces randomness of order of nodes/attributes
**                           in files produced by XmlRemapper.
** 005 SP  20250416          LinkedHashMap was replaced with CaseInsensitiveLinkedHashMap in attributes
**                           and child nodes storage. Removed toLowerCase() on node names.
*/

/*
** 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 com.goldencode.util.*;

import java.util.*;

/**
 * This class is used to hold node data - definition, attributes and list of
 * names of child nodes. It also implements most functionality required by
 * simple back-ends which hold data in memory. Derived classes may use
 * existing data members of this class (in particular <code>childList</code>)
 * to store child nodes, not just their names.
 * 
 * @author  SIY
 * @version 1.0
 */
class DirNode
{
   /** Current node attributes */
   private Map<String, Attribute> attributes;

   /** List of child nodes */
   protected Map<String, Object> childList;

   /** Node name */
   private String name;

   /** Object Class represented by node */
   protected ObjectClass nodeClass;

   /**
    * Constructs an empty node of given class and name. Note that constructed
    * node may be not valid until all mandatory attributes are filled.
    * 
    * @param   nodeClass
    *          Object Class of the created node.
    * @param   name
    *          Node name.
    */
   DirNode(ObjectClass nodeClass, String name)
   {
      this.nodeClass = nodeClass;
      attributes = Collections.synchronizedMap(new CaseInsensitiveLinkedHashMap<String, Attribute>());
      childList  = Collections.synchronizedMap(new CaseInsensitiveLinkedHashMap<String, Object>());

      setName(name);
   }

   /**
    * Constructs an empty node of given class name and name. Note that
    * constructed node may be not valid until all mandatory attributes are
    * filled.
    * 
    * @param   className
    *          Object Class name of the created node.
    * @param   name
    *          Node name.
    */
   DirNode(String className, String name)
   {
      this(SchemaStorage.getSchema().getObjectClass(className), name);
      ObjectClass oc = SchemaStorage.getSchema().getObjectClass(className);
      
      if (oc == null)
      {
         throw new RuntimeException("invalid directory class name " + className);
      }
   }
   
   /**
    * Constructs node as copy of other node.
    * 
    * @param   other
    *          <code>DirNode</code> instance to copy data from.
    */
   DirNode(DirNode other)
   {
      this(other.getNodeClass(), other.getName());
      
      String[] child = other.getChildNames();
      
      for (int i = 0; child != null && i < child.length; i++)
      {
         childList.put(child[i], child[i]);
      }

      replaceAttributes(other.getAttributes());
   }
   
   /**
    * Add attribute to the node. Replace existing attribute if it exists.
    * 
    * @param   attribute
    *          Attribute instance to add.
    *
    * @return  <code>true</code> if operation was successful.
    */
   boolean addAttribute(Attribute attribute)
   {
      if (attribute == null)
      {
         return false;
      }
      if (!attribute.isValid())
      {
         return false;
      }
      
      AttributeDefinition def;
      def = nodeClass.getAttributeDefinition(attribute.getName());
      if (def == null || !def.isCompatible(attribute.getDefinition()))
      {
         return false;
      }
      
      return true;
   }
   
   /**
    * Add array of attributes to the node. Replace existing if they exist and
    * remove existing attributes if they are not in array.
    * 
    * @param   attrList
    *          List of new node attributes.
    *
    * @return  <code>true</code> if operation was successful.
    */
   boolean replaceAttributes(Attribute[] attrList)
   {
      if (attrList == null)
      {
         return false;
      }
      
      for (int i = 0; i < attrList.length; i++)
      {
         if (attrList[i] == null || !attrList[i].isValid())
         {
            return false;
         }

         AttributeDefinition def;
         def = nodeClass.getAttributeDefinition(attrList[i].getName());
         if (def == null || !def.isCompatible(attrList[i].getDefinition()))
         {
            return false;
         }
      }

      attributes.clear();
      for (int i = 0; i < attrList.length; i++)
      {
         addAttributeInt(new Attribute(attrList[i]));
      }
      
      return true;
   }
   
   /**
    * Add new attribute value.
    * 
    * @param   attrName
    *          Attribute name
    * @param   value
    *          Attribute value
    * @param   initial
    *          Allow initialisation of the immutable attributes.
    *          <code>true</code> if this is initialisation mode.
    *
    * @return  <code>true</code> if operation was successful.
    */
   boolean addAttributeValue(String attrName, Object value, boolean initial)
   {
      if (attrName == null)
      {
         return false;
      }

      //Locate attribute definition
      AttributeDefinition def = nodeClass.getAttributeDefinition(attrName);
      if (def == null)
      {
         return false;
      }
      if (!initial && def.isImmutable())
      {
         return false;
      }

      Attribute attribute = getAttribute(attrName);
      if (attribute != null)
      {
         return attribute.addValue(value);
      }

      //Create new attribute
      attribute = new Attribute(def.genNodeAttribute(), new Object[]
         {
            value
         });
      addAttributeInt(attribute);

      return true;
   }

   /**
    * Add a new child node if this is allowed.
    * 
    * @param   child
    *          Child name.
    *
    * @return  <code>true</code> if operation was successful.
    */
   boolean addChild(String child)
   {
      if (child == null)
      {
         return false;
      }
      if (nodeClass.isClassLeaf())
      {
         return false;
      }
      if (childList.get(child) != null) //Node exists
      {
         return false;
      }
      childList.put(child, child);
      return true;
   }
   
   /**
    * Add new attribute value using <code>String</code> as a data source.
    * 
    * @param   attrName
    *          Attribute name
    * @param   value
    *          Attribute value
    *
    * @return  <code>true</code> if operation was successful.
    */
   boolean addStringAttributeValue(String attrName, String value)
   {
      if (attrName == null || value == null)
      {
         return false;
      }

      //Locate attribute definition
      AttributeDefinition def = nodeClass.getAttributeDefinition(attrName);
      if (def == null)
      {
         return false;
      }
      if (def.isImmutable())
      {
         return false;
      }
      Attribute attribute = getAttribute(attrName);
      if (attribute == null)
      {
         attribute = new Attribute(def.genNodeAttribute(), null);
         if (attribute.addStringValue(value))
         {
            addAttributeInt(attribute);
            return true;
         }
         return false;
      }
      return attribute.addStringValue(value);
   }

   /**
    * Get existing attribute or generate new empty instance of
    * <code>Attribute</code>.
    * 
    * @param   attrName
    *          Attribute name.
    *
    * @return  Reference to existing or created attribute instance.
    */
   Attribute genAttribute(String attrName)
   {
      Attribute attribute = getAttribute(attrName);
      if (attribute != null)
      {
         return attribute;
      }

      AttributeDefinition[] list = nodeClass.getClassDefinition();
      if (list == null)
      {
         return null;
      }
      for (int i = 0; i < list.length; i++)
      {
         if (list[i].getName().equals(attrName))
         {
            return new Attribute(list[i].genNodeAttribute(), null);
         }
      }
      return null;
   }

   /**
    * Get an attribute with specified name.
    * 
    * @param   name
    *          Name of the attribute to return.
    *
    * @return  Reference to attribute is such an attribute exists or
    *          <code>null</code> otherwise.
    */
   Attribute getAttribute(String name)
   {
      if (name == null)
      {
         return null;
      }
      return attributes.get(name);
   }

   /**
    * Get a list of all attributes.
    * 
    * @return  An array of all node attributes.
    */
   Attribute[] getAttributes()
   {
      return attributes.values().toArray(new Attribute[0]);
   }

   /**
    * Get number of children.
    * 
    * @return  Number of child nodes.
    */
   int getChildCount()
   {
      return childList.size();
   }

   /**
    * Get a list of all child node names.
    * 
    * @return  An array of all child names.
    */
   String[] getChildNames()
   {
      return childList.values().toArray(new String[0]);
   }

   /**
    * Get node name.
    * 
    * @return  Node name.
    */
   String getName()
   {
      return name;
   }

   /**
    * Get node Object Class.
    * 
    * @return  ObjectClass instance assigned to node.
    */
   ObjectClass getNodeClass()
   {
      return nodeClass;
   }

   /**
    * Check if child node with specified name exists.
    * 
    * @param    name
    *           Child node name.
    *           
    * @return   <code>true</code> if child with specified name exists.
    */
   boolean hasChild(String name)
   {
      return childList.containsKey(name);
   }
   
   /**
    * Check if node is of specified Object Class.
    * 
    * @param   className
    *          An object class name to compare with.
    *
    * @return  <code>true</code> if node is of specified object class and
    *          <code>false</code> otherwise.
    */
   boolean isA(String className)
   {
      return nodeClass.getName().equals(className);
   }
   
   /**
    * Verify validness of the node. Node is considered valid if all mandatory
    * attributes are set.
    * 
    * @return  <code>true</code> if node is valid.
    */
   boolean isValid()
   {
      AttributeDefinition[] list = nodeClass.getClassDefinition();
      if (list == null && attributes.size() == 0)
      {
         return true;
      }
      if (list == null)
      {
         return true;
      }
      for (int i = 0; i < list.length; i++)
      {
         Attribute attribute = getAttribute(list[i].getName());
         if (attribute == null)
         {
            if (!list[i].isMandatory())
            {
               continue;
            }
            //Attribute is mandatory but it is missing
            return false;
         }
         if (!attribute.isValid())
         {
            return false;
         }
      }
      return true;
   }

   /**
    * Remove attribute with specified name.
    * 
    * @param   name
    *          Name of the attribute to remove.
    *
    * @return  <code>true</code> if operation was successful.
    */
   boolean removeAttribute(String name)
   {
      if (name == null) //No such attribute
      {
         return false;
      }
      Attribute attr = getAttribute(name);
      if (attr == null) //No such attribute
      {
         return false;
      }
      if (attr.getDefinition().isMandatory())
      {
         return false;
      }
      attributes.remove(name);
      return true;
   }

   /**
    * Add or replace attribute with specified one.
    * 
    * @param   attribute
    *          An attribute to add or replace existing.
    *
    * @return  <code>true</code> if operation was successful.
    */
   boolean setAttribute(Attribute attribute)
   {
      if (attribute == null)
      {
         return false;
      }
      if (getAttribute(attribute.getName()) == null)
      {
         //There is no such attribute so we need to check if such
         //an attribute can be added.
         AttributeDefinition def;
         def = nodeClass.getAttributeDefinition(attribute.getName());
         if (def == null || !def.isCompatible(attribute.getDefinition()))
         {
            return false;
         }
      }
      addAttributeInt(attribute);
      return true;
   }

   /**
    * Set new attribute value.
    * 
    * @param   attrName
    *          Attribute name
    * @param   value
    *          New value for the attribute
    * @param   ndx
    *          Index of the value
    *
    * @return  <code>true</code> if operation was successful.
    */
   boolean setAttributeValue(String attrName, Object value, int ndx)
   {
      if (attrName == null)
      {
         return false;
      }
      Attribute attribute = getAttribute(attrName);
      if (attribute == null)
      {
         return false;
      }
      //Locate attribute definition
      AttributeDefinition def = nodeClass.getAttributeDefinition(attrName);
      if (def == null)
      {
         return false;
      }
      if (def.isImmutable())
      {
         return false;
      }
      return attribute.setValue(ndx, value);
   }

   /**
    * Rename node.
    * 
    * @param   name
    *          New name for the node.
    */
   void setName(String name)
   {
      this.name = new String(name);
   }

   /**
    * Check if object class of the node does not allow adding child nodes.
    * 
    * @return  <code>true</code> if object class is leaf.
    */
   boolean isLeaf()
   {
      return nodeClass.isClassLeaf();
   }
   
   /**
    * Remove child with specified name.
    * 
    * @param   child
    *          Name of the child to remove.
    *
    * @return  <code>true</code> if operation was successful.
    */
   boolean removeChild(String child)
   {
      if (child == null || childList.get(child) == null) //No such child
      {
         return false;
      }
      childList.remove(child);
      return true;
   }

   /**
    * Convenience method which adds attribute into the set of node attributes.
    * 
    * @param   attribute
    *          Attribute to add.
    */
   private void addAttributeInt(Attribute attribute)
   {
      attributes.put(attribute.getName(), attribute);
   }
}