Attribute.java

/*
** Module   : Attribute.java
** Abstract : An attribute container.
**
** Copyright (c) 2005-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- ----------------Description----------------------
** 001 SIY 20050216  @20011  Created initial implementation
** 002 SIY 20050308  @20351  Organized imports and reordered methods.
** 003 SIY 20050323  @20464  Fixed formatting.
** 004 SIY 20050426  @21003  Yet another pass on formatting to make it
**                           compliant to coding standards. Added isA().
** 005 NVS 20050426  @20889  Class implements Serializable interface.
** 006 SIY 20050514  @21184  More detailed documentation.
** 007 NVS 20070322  @32539  getValues() and getStringValue() are public
**                           methods now.
** 008 NVS 20090625  @42959  Protected the constructors against null arguments
**                           with the advice to check the directory schema file
** 009 NVS 20090924  @44026  Attributes can be instantiated without values in
**                           order to allow gradual value creation.
** 010 ECF 20150715          Replace StringBuffer with StringBuilder.
** 011 HC  20170612          Added support of storing com.goldencode.p2j.security.BitSet
**                           and java.util.Date types in the directory.
** 012 VVT 20200203          The getValue() does not use NPE just for test anymore.
**                           Which makes debugging a less annoying.
** 013 IAS 20200914          Re-work (de)serialization
**     TJD 20220504          Upgrade do Java 11
** 014 GBB 20230512          Logging methods replaced by CentralLogger/ConversionStatus.
** 015 SP  20250416          Code formatting 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 static com.goldencode.util.NativeTypeSerializer.*;
import com.goldencode.p2j.security.*;
import com.goldencode.p2j.security.BitSet;
import com.goldencode.p2j.util.*;
import com.goldencode.p2j.util.logging.*;

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

/**
 * An attribute container class.
 * <p>
 * This class contains attribute values and attribute definition and is used
 * to transfer or hold particular attribute of particular node. Using
 * attribute definition it can verify current attribute content for the
 * validness. It also checks new attribute values for correct type to ensure
 * that attribute contains only values of specified type.
 *
 * @author  SIY
 * @version 1.0
 */
public class Attribute
implements Externalizable
{
   /** Logger */
   private static final CentralLogger LOG = CentralLogger.get(Attribute.class);

   /** Attribute definition details */
   private NodeAttribute definition;

   /** Storage for the attribute values */
   private Vector<Object> values;

   /**
    * Construct an attribute as copy of other attribute
    *
    * @param   other
    *          An instance to get information from.
    */
   public Attribute(Attribute other)
   {
      // protection
      if (other == null)
      {
         throw new IllegalArgumentException(
                   "Source attribute must be provided");
      }

      this.definition = new NodeAttribute(other.definition);
      this.values = (Vector<Object>) other.values.clone();
   }

   /**
    * Construct an attribute and assign a set of values to the attribute. Note
    * that if definition contains mandatory attributes but no values are
    * provided for the attribute then attribute will be invalid. Existence of
    * such an attributes is allowed because in some cases attribute is built
    * in several steps.
    *
    * @param   definition
    *          Attribute definition.
    * @param   list
    *          An array of values which will be assigned to the attribute.
    */
   public Attribute(NodeAttribute definition, Object[] list)
   {
      // protection
      if (definition == null)
      {
         throw new IllegalArgumentException("Unknown attribute definition. Check the directory schema.");
      }

      this.definition = new NodeAttribute(definition);
      this.values = new Vector<>();

      if (list != null)
      {
         for (int i = 0; i < list.length; i++)
         {
            if (list[i] == null)
            {
               continue;
            }

            boolean rc = addValue(list[i]);
            if (!rc)
            {
               throw new IllegalArgumentException("Attribute value is incorrect");
            }
         }
      }
   }

   /**
    * Add an value to the attribute. If attribute supports multiple values
    * then parameter is added at the end of the list of values. If attribute
    * does not support multiple values and there is no value assigned to the
    * attribute then provided parameter is assigned to attribute value. If
    * parameter is of incorrect type or attribute does not support multiple
    * values and single value is already assigned then parameter is ignored
    * and <code>false</code> is returned.
    *
    * @param   value
    *          A value to add.
    * @return  result of the operation -<code>true</code> if success and
    *          <code>false</code> if error occurred.
    */
   public boolean addValue(Object value)
   {
      if (value == null)
      {
         return false;
      }

      value = convertValue(value);
      if (!checkType(value))
      {
         return false;
      }

      if (definition.isMultiple() || values.size() == 0)
      {
         try
         {
            values.add(value);
            definition.setCount(values.size());
            return true;
         }
         catch (Exception e)
         {
            LOG.log(Level.FINE, "", e);
         }
      }

      return false;
   }

   /**
    * Delete attribute value at specified index.
    *
    * @param   index
    *          An index of variable to remove.
    * @return  result of the operation -<code>true</code> if success and
    *          <code>false</code> if error occurred.
    */
   public boolean deleteValue(int index)
   {
      if (getCount() == 1 && index == 0 && definition.isMandatory())
      {
         //Do not allow removing last mandatory attribute
         return false;
      }

      //OK, it is safe to try to delete value
      try
      {
         values.removeElementAt(index);
         definition.setCount(values.size());
         return true;
      }
      catch (Exception e)
      {
      }
      return false;
   }

   /**
    * Get number of values assigned to the attributed.
    *
    * @return  number of elements in the list of values.
    */
   public int getCount()
   {
      return values.size();
   }

   /**
    * Provide access to the NodeAttribute definition.
    *
    * @return  a definition used by this attribute.
    */
   public NodeAttribute getDefinition()
   {
      return definition;
   }

   /**
    * Simplify access to Object Class name.
    *
    * @return  Object Class name from definition.
    */
   public String getName()
   {
      return definition.getName();
   }

   /**
    * Convenience method for the <code>NodeAtribute.getType()</code>.
    *
    * @return  attribute type.
    */
   public int getType()
   {
      return definition.getType();
   }

   /**
    * Compare attribute type with provided type and return <code>true</code>
    * if they match.
    *
    * @param   type
    *          Attribute type.
    *
    * @return  <code>true</code> if given type match attribute type.
    */
   public boolean isA(int type)
   {
      return (definition.getType() == type);
   }

   /**
    * Return <code>true</code> if attribute can contain multiple values.
    *
    * @return  <code>true</code> if attribute can contain multiple values.
    */
   public boolean isMultiple()
   {
      return definition.isMultiple();
   }

   /**
    * Returns the indexed value of the attribute as a generic object, that can
    * be cast into its primitive data type.
    *
    * @param   index
    *          Index of the value to return. 0 - for first value (or single
    *          value if attribute does not support multiple values).
    *
    * @return  value of the attribute
    */
   public Object getValue(int index)
   {
      if(index < 0 || index >= values.size())
      {
         return null;
      }
      return values.elementAt(index);
   }

   /**
    * Check if attribute is valid, for example attributes with mandatory flag
    * set at have least one value is assigned.
    *
    * @return  <code>true</code> if attribute is valid.
    */
   public boolean isValid()
   {
      if (!definition.isMandatory())
      {
         return true;
      }
      return (getCount() > 0);
   }

   /**
    * Set new value to attribute at specified index. If parameter is of
    * incorrect type or attribute does not support multiple values or there is
    * no assigned value at specified index then parameter is ignored and
    * <code>false</code> is returned.
    *
    * @param   index
    *          An index of variable to set.
    * @param   value
    *          A value to set.
    *
    * @return  result of the operation -<code>true</code> if success and
    *          <code>false</code> if error occurred.
    */
   public boolean setValue(int index, Object value)
   {
      if (value == null)
      {
         return false;
      }

      value = convertValue(value);
      if (!checkType(value))
      {
         return false;
      }

      try
      {
         values.setElementAt(value, index);
         return true;
      }
      catch (Exception e)
      {
      }
      return false;
   }

   /**
    * Export attribute in human-readable form.
    *
    * @see     java.lang.Object#toString()
    *
    * @return  Attribute data as human-readable <code>String</code>.
    */
   public String toString()
   {
      Object[] vals = toArray(new Object[getCount()]);
      StringBuilder res = new StringBuilder();

      res.append("attribute <");
      res.append(getName());
      res.append("> {");

      for (int i = 0; i < vals.length; i++)
      {
         res.append(vals[i].toString());
         if (i != (vals.length - 1))
         {
            res.append(", ");
         }
      }
      res.append("}");

      return res.toString();
   }

   /**
    * Add an value to the attribute. Value is restored from the String
    * parameter. If parameter is of incorrect type or attribute does not
    * support multiple values and single value is already assigned then
    * parameter is ignored and <code>false</code> is returned.
    *
    * @param   str
    *          A value to add.
    *
    * @return  result of the operation -<code>true</code> if success and
    *          <code>false</code> if error occurred.
    */
   boolean addStringValue(String str)
   {
      Object value = stringToValue(str);
      if (value == null)
      {
         return false;
      }

      if (!checkType(value))
      {
         return false;
      }

      if (definition.isMultiple() || values.size() == 0)
      {
         try
         {
            values.add(value);
            definition.setCount(values.size());
            return true;
         }
         catch (Exception e)
         {
         }
      }

      return false;
   }

   /**
    * Return attribute values as array of strings.
    *
    * @return  An array of string representation of the attribute values.
    */
   public String[] getValues()
   {
      String[] res = new String[values.size()];
      for (int i = 0; i < res.length; i++)
      {
         res[i] = getStringValue(i);
      }
      return res;
   }

   /**
    * Convert attribute value into string.
    *
    * @param   index
    *          An index to check.
    *
    * @return  Text representation of the attribute value.
    */
   public String getStringValue(int index)
   {
      return getStringValue(values.elementAt(index));
   }

   /**
    * Convert given object into string.
    *
    * @param   value
    *          A value to check.
    *
    * @return  Text representation of the value.
    */
   String getStringValue(Object value)
   {
      if (value == null)
      {
         return null;
      }
      value = convertValue(value);

      if (definition.getType() == AttributeType.ATTR_BOOLEAN)
      {
         return value.toString().toUpperCase();
      }
      if (definition.getType() == AttributeType.ATTR_BYTEARRAY)
      {
         return Base64.byteArrayToBase64((byte[]) value);
      }
      if (definition.getType() == AttributeType.ATTR_BITSELECTOR ||
          definition.getType() == AttributeType.ATTR_BITFIELD)
      {
         return "'" + value.toString() + "'B";
      }

      return value.toString();
   }

   /**
    * Get all values in one array.
    *
    * @param   a
    *          The array into which the elements are to be stored, if it is
    *          big enough; otherwise, a new array of the same run-time type
    *          is allocated for this purpose.
    *
    * @return  All values in one array or <code>null</code>.
    */
   Object[] toArray(Object[] a)
   {
      if (values.size() == 0)
         return null;

      //Make sure that types match
      if (!a.getClass().getComponentType().isInstance(getVariable()))
         return null;

      return values.toArray(a);
   }

   /**
    * Check that provided object match the attribute definition.
    *
    * @param   value
    *          An object to check
    *
    * @return  <code>true</code> if parameter has valid type.
    */
   private boolean checkType(Object value)
   {
      if (value == null)
      {
         return false;
      }
      return value.getClass().isInstance(getVariable());
   }

   /**
    * This method returns a default value for the specified NodeAttribute
    * definition.
    *
    * @return  An instance of object of valid type for definition provided
    *          during attribute creation.
    */
   private Object getVariable()
   {
      switch (definition.getType())
      {
         case AttributeType.ATTR_INTEGER:
            return 0;

         case AttributeType.ATTR_BOOLEAN:
            return false;

         case AttributeType.ATTR_STRING:
            return new String("");

         case AttributeType.ATTR_DOUBLE:
            return 0.0d;

         case AttributeType.ATTR_BYTEARRAY:
            return new byte[0];

         case AttributeType.ATTR_BITFIELD:
            return new BitField(1);

         case AttributeType.ATTR_BITSELECTOR:
            return new BitSelector(1, 0);

         case AttributeType.ATTR_DATE:
            return new DateValue();

         case AttributeType.ATTR_TIME:
            return new TimeValue(0, 0);
      }
      return null;
   }

   /**
    * Convert given <code>String</code> into value using attribute type
    * definition to find conversion routine.
    *
    * @param   str
    *          Source string
    *
    * @return  Object of correct type or <code>null</code> is something is
    *          wrong with the source string.
    */
   private Object stringToValue(String str)
   {
      if (str == null)
      {
         return null;
      }
      try
      {
         switch (definition.getType())
         {
            case AttributeType.ATTR_STRING:
               return str;

            case AttributeType.ATTR_BYTEARRAY:
               return Base64.base64ToByteArray(str);

            case AttributeType.ATTR_INTEGER:
               return Integer.valueOf(str, 10);

            case AttributeType.ATTR_BOOLEAN:
               return Boolean.valueOf(str);

            case AttributeType.ATTR_DOUBLE:
               return Double.valueOf(str);

            case AttributeType.ATTR_BITFIELD:
               return BitField.valueOf(str);

            case AttributeType.ATTR_BITSELECTOR:
               return BitSelector.valueOf(str);

            case AttributeType.ATTR_DATE:
               return DateValue.valueOf(str);

            case AttributeType.ATTR_TIME:
               return TimeValue.valueOf(str);
         }
      }
      catch (Exception e)
      {
         LOG.log(Level.FINE, "", e);
      }
      return null;
   }

   /**
    * Converts a value of supported data type into a value of a native data type.
    * <p>
    * This method currently supports conversion of {@link BitSet} data type into
    * {@link BitField}.
    *
    * @param    value
    *           The value to convert.
    *
    * @return   The converted result.
    */
   private Object convertValue(Object value)
   {
      if (value == null)
      {
         return null;
      }

      if (value instanceof BitSet)
      {
         return BitSetHelper.toBitField((BitSet) value);
      }
      if (value instanceof Date)
      {
         return new DateValue((Date) value);
      }
      else
      {
         return value;
      }
   }

   /**
    * Replacement for the default object writing method.
    *
    * @param    out
    *           The output destination to which fields will be saved.
    *
    * @throws   IOException
    *           In case of I/O errors.
    */
   @Override
   public void writeExternal(ObjectOutput out) throws IOException
   {
      writeExternalizable(out, definition);
      writeList(out, ObjectOutput::writeObject, values);
   }

   /**
    * Replacement for the default object reading method.
    *
    * @param    in
    *           Input source from which fields will be restored.
    *
    * @throws   IOException
    *           In case of I/O errors.
    * @throws   ClassNotFoundException
    *           If payload can't be instantiated.
    */
   @Override
   public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException
   {
      definition = readExternalizable(in, NodeAttribute::new);
      values = readList(in, Vector::new, ObjectInput::readObject);
   }
}