PropertyDefinition.java

/*
** Module   : PropertyDefinition.java
** Abstract : A container for a DMO property's metadata.
**
** Copyright (c) 2013-2022, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
** 001 CA  20130618 Created initial version.
** 002 ECF 20140623 Removed Hibernate dependency. This update simplifies the use of the type
**                  variable; this is a short-term workaround which breaks some result set
**                  metadata functionality (not currently in use), but it needs further analysis/work.
** 003 ECF 20140626 Simplified type check.
** 004 SVL 20140709 Added legacyName field.
** 005 CA  20190812 Added toString().
** 006 IAS 20200908 Rework (de)serialization.
** 007 IAS 20200922 Get rid of possible NPE on serialization.
**     SVL 20201030 Added format, label and columnLabel fields.
**     OM  20201120 Added SCHEMA-MARSHAL implementation.
**     CA  20220909 Always marshal the schema, otherwise the remote side will not be able to rebuild the table.
**     IAS 20221029 Added support for missed properties.
**     IAS 20221102 Re-worked serialization for SCHEMA-MARSHAL == NONE.
*/

/*
** 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.persist;

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

import com.goldencode.p2j.persist.orm.*;
import com.goldencode.p2j.util.*;

import static com.goldencode.p2j.persist.AbstractTempTable.*;
import static com.goldencode.util.NativeTypeSerializer.*; 

/**
 * Container for a DMO property's metadata: name, type and extent.  Used to send the metadata of a
 * DMO to a remote side, when remote appserver calls are involved.
 * 
 * TODO: add the other schema attributes of a property definition: help, initial, mandatory, validateMessage,
 *       validationExpression, description, decimals (for decimal type), caseSensitive (for character type)
 *       codePage, serializeName, xmlNodeName, xmlDataType, etc.
 *       All these will be serialized only if {@code schemaMarshalLevel == SM_FULL}.
 */
public class PropertyDefinition
implements Externalizable
{
   /** Map of primitive names to their classes. */
   private static final Map<String, Class<?>> primitiveClasses = new HashMap<>();
   
   /** Default value for no-extent properties. */
   private static final int NO_EXTENT = -2;
   
   /** The P2J wrapper type of this property. */
   private Class<?> type;
   
   /** The extent of this property, or {@link #NO_EXTENT} if the property has no extent. */
   private int extent;
   
   /** The name of this property. */
   private String name;
   
   /** The legacy name of this property. */
   private String legacyName;
   
   /** The format of this property. */
   private String format;
   
   /** The label of this property. */
   private String label;
   
   /** The column label of this property. */
   private String columnLabel;

   /** HELP attribute of this field. */
   private String help;
   
   /** INITIAL attribute of this field. */
   private String initial;

   /** Flag indicating the field should not be included in serialized output. */
   private boolean serializeHidden;
   
   /** Flag indicating the field is case-sensitive. */
   private boolean caseSensitive;

   /** Node type of field in XML output. */
   private String xmlNodeType;
   
   /** Code page of the CLOB field. */
   private String codePage;

   /**
    * The SCHEMA-MARSHAL level for the parent  temp-table. It is never {@code SM_DEFAULT} when object is to
    * be serialized. Always {@code SM_DEFAULT} for a read object.
    */
   private int schemaMarshalLevel = SM_DEFAULT;
   
   static
   {
       primitiveClasses.put("byte", byte.class);
       primitiveClasses.put("short", short.class);
       primitiveClasses.put("char", char.class);
       primitiveClasses.put("int", int.class);
       primitiveClasses.put("long", long.class);
       primitiveClasses.put("float", float.class);
       primitiveClasses.put("double", double.class);
   }
   
   /**
    * Default c'tor, explicitly added to allow instances of this class to be created on 
    * deserialization. Not for public use.
    */
   public PropertyDefinition()
   {
   }
   
   /**
    * Create a new, no-extent, property definition.
    * 
    * @param    name
    *           The name of this property.
    * @param    type
    *           The type of this property.
    *
    * @throws   ClassNotFoundException
    *           If the given <code>type</code> can not be resolved to a valid class.
    */
   public PropertyDefinition(String name, String type)
   throws ClassNotFoundException
   {
      this.name = name;
      Class<?> cls = primitiveClasses.containsKey(type) ? primitiveClasses.get(type) : Class.forName(type);
      verifyType(cls);
      this.type = cls;
      this.extent = NO_EXTENT;
   }
   
   /**
    * Create a new property definition.
    * 
    * @param    name
    *           The name of this property.
    * @param    type
    *           The type of this property.
    * @param    extent
    *           The extent of this property.
    *
    * @throws   ClassNotFoundException
    *           If the given {@code type} can not be resolved to a valid class.
    */
   public PropertyDefinition(String name, String type, int extent)
   throws ClassNotFoundException
   {
      this(name, type);
      this.extent = extent;
   }
   
   /**
    * Create a new, no-extent, property definition.
    * 
    * @param    name
    *           The name of this property.
    * @param    type
    *           A class specifying the type of this property.
    */
   public PropertyDefinition(String name, Class<?> type)
   {
      this(name, type, null);
   }

   /**
    * Create a new, no-extent, property definition.
    *
    * @param    name
    *           The name of this property.
    * @param    type
    *           A class specifying the type of this property.
    * @param    legacyName
    *           The legacy name of this property.
    */
   public PropertyDefinition(String name, Class<?> type, String legacyName)
   {
      this(name, type, legacyName, null, null, null);
   }
   
   /**
    * Create a new property definition.
    * 
    * @param    name
    *           The name of this property.
    * @param    type
    *           A class specifying the type of this property.
    * @param    extent
    *           The extent of this property.
    */
   public PropertyDefinition(String name, Class<?> type, int extent)
   {
      this(name, type, extent, null);
   }

   /**
    * Create a new property definition.
    *
    * @param    name
    *           The name of this property.
    * @param    type
    *           A class specifying the type of this property.
    * @param    extent
    *           The extent of this property.
    * @param    legacyName
    *           The legacy name of this property.
    */
   public PropertyDefinition(String name, Class<?> type, int extent, String legacyName)
   {
      this(name, type, extent, legacyName, null, null, null);
   }

   /**
    * Create a new, no-extent, property definition.
    *
    * @param    name
    *           The name of this property.
    * @param    type
    *           A class specifying the type of this property.
    * @param    legacyName
    *           The legacy name of this property.
    * @param    format
    *           The format of this property.
    * @param    label
    *           The label of this property.
    * @param    columnLabel
    *           The column label of this property.
    */
   public PropertyDefinition(String name,
                             Class<?> type,
                             String legacyName,
                             String format,
                             String label,
                             String columnLabel)
   {
      this(name, type, NO_EXTENT, legacyName, format, label, columnLabel);
   }

   /**
    * Create a new property definition.
    *
    * @param    name
    *           The name of this property.
    * @param    type
    *           A class specifying the type of this property.
    * @param    legacyName
    *           The legacy name of this property.
    * @param    format
    *           The format of this property.
    * @param    label
    *           The label of this property.
    * @param    columnLabel
    *           The column label of this property.
    */
   public PropertyDefinition(String name,
                             Class<?> type,
                             int extent,
                             String legacyName,
                             String format,
                             String label,
                             String columnLabel)
   {
      this.name = name;
      verifyType(type);
      this.type = type;
      this.extent = extent;
      this.legacyName = legacyName;
      this.format = format;
      this.label = label;
      this.columnLabel = columnLabel;
   }

   /**
    * Create a new property definition.
    *
    * @param    prop
    *           The ORM field <code>Property</code>.
    */           
   public PropertyDefinition(Property prop)
   {
      this.name = prop.name;
      verifyType(prop._fwdType);
      this.type = prop._fwdType;
      int extent = prop.index > 0 ? 0 : prop.extent;
      this.extent = extent == 0 ? NO_EXTENT : extent;
      this.legacyName = prop.legacy;
      this.format = prop.format;
      this.label = prop.label;
      this.columnLabel = prop.columnLabel;
      this.help = prop.help;
      this.initial = prop.initial;
      this.serializeHidden = prop.serializeHidden;
      this.caseSensitive = prop.caseSensitive;
      this.xmlNodeType = prop.xmlNodeType;
      this.codePage = prop.codePage;
   }
   /**
    * Get the type of this property.
    * 
    * @return   See above.
    */
   public Class<?> getType()
   {
      return type;
   }
   
   /**
    * Get the extent of this property.
    * 
    * @return   See above.
    */
   public int getExtent()
   {
      return extent;
   }

   /**
    * Get the name of this property.
    * 
    * @return   See above.
    */
   public String getName()
   {
      return name;
   }
   
   /**
    * Check if this property is an extent property.
    * 
    * @return   <code>true</code> if this property is an extent property.
    */
   public boolean isExtent()
   {
      return extent != NO_EXTENT;
   }

   /**
    * Get legacy name of this property.
    *
    * @return legacy name of this property.
    */
   public String getLegacyName()
   {
      return legacyName;
   }

   /**
    * Get format of this property.
    *
    * @return format of this property.
    */
   public String getFormat()
   {
      return format;
   }

   /**
    * Get label of this property.
    *
    * @return label of this property.
    */
   public String getLabel()
   {
      return label;
   }

   /**
    * Get column label of this property.
    *
    * @return column label of this property.
    */
   public String getColumnLabel()
   {
      return columnLabel;
   }

   /**
    * Get help text of this property.
    *
    * @return help text of this property.
    */
   public String getHelp()
   {
      return help;
   }

   /**
    * Get initial value of this property.
    *
    * @return initial value of this property.
    */
   public String getInitial()
   {
      return initial;
   }

   /**
    * Get 'serialize hidden' flag of this property.
    *
    * @return 'serialize hidden' flag of this property.
    */
   public boolean isSerializeHidden()
   {
      return serializeHidden;
   }

   /**
    * Get 'case-sensitive' flag of this property.
    *
    * @return 'case-sensitive' flag of this property.
    */
   public boolean isCaseSensitive()
   {
      return caseSensitive;
   }

   
   /**
    * Get XML node type of this property.
    *
    * @return XML node type of this property.
    */
   public String getXmlNodeType()
   {
      return xmlNodeType;
   }

   /**
    * Get code page of this property.
    *
    * @return code page of this property.
    */
   public String getCodePage()
   {
      return codePage;
   }

   /**
    * Send the property definition to the specified output destination.
    * 
    * @param    out
    *           The output destination to which the property definition will be sent.
    *
    * @throws   IOException
    *           In case of I/O errors.
    */
   public final void writeExternal(ObjectOutput out)
   throws IOException
   {
      if (schemaMarshalLevel == SM_NONE)
      {
         throw new IllegalStateException("Should not be called if schemaMarshalLevel == SM_NONE");
      }
      boolean full = schemaMarshalLevel == SM_FULL || schemaMarshalLevel == SM_DEFAULT;
      
      writeString(out, name);
      writeString(out, legacyName);
      writeString(out, type == null ? null : type.getName());
      out.writeInt(extent);
      writeString(out, initial);
      writeString(out, xmlNodeType);
      writeString(out, codePage);
      int flags = (serializeHidden ? 1 : 0) | (caseSensitive ? 2 : 0) | (full ? 4 : 0);
      out.writeByte(flags);
      if (!full )
      {
         return;
      }
      writeString(out, format);
      writeString(out, label);
      writeString(out, columnLabel);
      writeString(out, help);
   }
   
   /**
    * Read the property definition from the specified input source.
    * 
    * @param    in
    *           The input source from which the property definition will be read.
    *
    * @throws   IOException
    *           In case of I/O errors.
    *
    * @throws  ClassNotFoundException
    *          If the class of property could not be found/loaded.
    */
   public final void readExternal(ObjectInput in)
   throws IOException,
          ClassNotFoundException
   {
      name = readString(in);
      legacyName = readString(in);
      String tn = readString(in);
      type = (tn == null) ? null : Class.forName(tn);
      extent = in.readInt();
      initial = readString(in);
      xmlNodeType = readString(in);
      codePage = readString(in);
      int flags = in.readByte();
      serializeHidden = (flags & 1) != 0;
      caseSensitive = (flags & 2) != 0;
      boolean full = (flags & 4) != 0;
      if (!full)
      {
         return;
      }
      format = readString(in);
      label = readString(in);
      columnLabel = readString(in);
      help = readString(in);
   }
   
   /**
    * Verify the given type is not null.
    *
    * @param    type
    *           The type to check.
    *
    * @throws   NullPointerException
    *           If <code>type</code> is null.
    */
   private void verifyType(Class<?> type)
   {
      if (type == null)
      {
         throw new NullPointerException("Type may not be null");
      }
   }
   
   /**
    * Get a string representation of this property.
    * 
    * @return    See above.
    */
   @Override
   public String toString()
   {
      StringBuilder sb = new StringBuilder();
      sb.append(name).append("[legacy: ").append(legacyName).append("]: ").append(type.getSimpleName());
      if (extent != NO_EXTENT && extent != 0)
      {
         sb.append("[").append(extent).append("]");
      }
      
      return sb.toString();
   }
   
   /**
    * Configures the SCHEMA-MARSHAL level used for serialization of the property.
    * 
    * @param   schemaMarshalLevel
    *          The new SCHEMA-MARSHAL level. See {@link AbstractTempTable} static constants for details.
    */
   public void setMarshalLevel(int schemaMarshalLevel)
   {
      this.schemaMarshalLevel = schemaMarshalLevel;
   }
}