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